lapy 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lapy-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Deep Medical Imaging Lab (PI Reuter)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
lapy-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.1
2
+ Name: lapy
3
+ Version: 0.3.0
4
+ Summary: A package for differential geometry on meshes (Laplace, FEM)
5
+ Author-email: Martin Reuter <martin.reuter@dzne.de>
6
+ Maintainer-email: Martin Reuter <martin.reuter@dzne.de>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2020 Deep Medical Imaging Lab (PI Reuter)
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: homepage, https://github.com/Deep-MI/LaPy
30
+ Project-URL: documentation, https://github.com/Deep-MI/LaPy
31
+ Project-URL: source, https://github.com/Deep-MI/LaPy
32
+ Project-URL: tracker, https://github.com/Deep-MI/LaPy/issues
33
+ Keywords: python,Laplace,FEM,ShapeDNA,BrainPrint,Triangle Mesh,Tetrahedra Mesh,Geodesics in Heat,Mean Curvature Flow
34
+ Classifier: Operating System :: Microsoft :: Windows
35
+ Classifier: Operating System :: Unix
36
+ Classifier: Operating System :: MacOS
37
+ Classifier: Programming Language :: Python :: 3 :: Only
38
+ Classifier: Programming Language :: Python :: 3.8
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Natural Language :: English
43
+ Classifier: License :: OSI Approved :: MIT License
44
+ Classifier: Intended Audience :: Science/Research
45
+ Requires-Python: >=3.8
46
+ Description-Content-Type: text/markdown
47
+ Provides-Extra: build
48
+ Provides-Extra: chol
49
+ Provides-Extra: style
50
+ Provides-Extra: test
51
+ Provides-Extra: all
52
+ Provides-Extra: full
53
+ License-File: LICENSE
54
+
55
+ # LaPy
56
+
57
+ LaPy is a package to compute spectral features (Laplace-Beltrami operator) on
58
+ tetrahedral and triangle meshes. It is written purely in python 3 without
59
+ sacrificing speed as almost all loops are vectorized, drawing upon efficient
60
+ and sparse mesh data structures. It is basically a port of the C++ ShapeDNA
61
+ project with extended differential geometry capabilities.
62
+
63
+ ## Contents:
64
+
65
+ - TriaMesh: a class for triangle meshes offering various operations, such as
66
+ fixing orientation, smoothing, curvature, boundary, quality, normals, and
67
+ various efficient mesh datastructure (edges, adjacency matrices)
68
+ - TetMesh: a class for tetrahedral meshes (orientation, boundary ...)
69
+ - TriaIO, TetIO: for both tets and trias from off, vtk, etc. formats
70
+ - FuncIO: import/export vertex functions and eigenvector files
71
+ - Solver: a class for linear FEM computation (Laplace stiffness and mass
72
+ matrix, fast and sparse eigenvalue solver, anisotropic Laplace, Poisson)
73
+ - DiffGeo: compute gradients, divergence, mean curvature flow, etc.
74
+ - Heat: for heat kernel and diffusion
75
+ - ShapeDNA: compute the ShapeDNA descriptor of surfaces and solids
76
+ - Plot: functions for interactive visualization (wrapping plotly)
77
+
78
+ ## ToDo:
79
+
80
+ - Add unit tests and automated testing (e.g. travis)
81
+ - Add command line scripts for various functions
82
+
83
+ ## Usage:
84
+
85
+ The LaPy package is a comprehensive collection of scripts, so we refer to the
86
+ 'help' function and docstring of each module / function / class for usage info.
87
+ For example:
88
+
89
+ ```
90
+ import lapy as lp
91
+ help(lp.TriaMesh)
92
+ help(lp.Solver)
93
+ ```
94
+
95
+ In the `examples` subdirectory, we provide several Jupyter notebooks that
96
+ illustrate prototypical use cases of the toolbox.
97
+
98
+ ## Installation:
99
+
100
+ Use the following code to download, build and install a package from this
101
+ repository into your local Python package directory:
102
+
103
+ `pip3 install --user git+https://github.com/Deep-MI/LaPy.git#egg=lapy`
104
+
105
+ Use the following code to install the package in editable mode to a location of
106
+ your choice:
107
+
108
+ `pip3 install --user --src /my/preferred/location --editable git+https://github.com/Deep-MI/Lapy.git#egg=lapy`
109
+
110
+ Several functions, e.g. the Solver, require a sparse matrix decomposition, for which either the LU decomposition (from scipy sparse) or the faster Cholesky decomposition (from scikit-sparse cholmod) can be used. If the parameter flag use_cholmod is True, the code will try to import cholmod from the scikit-sparse package and will fall back to LU if the import fails. If you would like to use cholmod, you need to install scikit-sparse separately. It cannot be listed among LaPy's dependencies (e.g. in setup.py or requirements.txt) as that causes errors with pip. scikit-sparse requires numpy and scipy to be installed separately beforehand.
111
+
112
+ ## References:
113
+
114
+ If you use this software for a publication please cite both these papers:
115
+
116
+ [1] Laplace-Beltrami spectra as 'Shape-DNA' of surfaces and solids. Reuter M, Wolter F-E, Peinecke N Computer-Aided Design. 2006;38(4):342-366. http://dx.doi.org/10.1016/j.cad.2005.10.011
117
+
118
+ [2] BrainPrint: a discriminative characterization of brain morphology. Wachinger C, Golland P, Kremen W, Fischl B, Reuter M Neuroimage. 2015;109:232-48. http://dx.doi.org/10.1016/j.neuroimage.2015.01.032 http://www.ncbi.nlm.nih.gov/pubmed/25613439
119
+
120
+ [1] introduces the FEM methods and the Laplace spectra for shape analysis, while [2] focusses on medical applications.
121
+
122
+ For Geodesics please cite:
123
+
124
+ [3] Crane K, Weischedel C, Wardetzky M. Geodesics in heat: A new approach to computing distance based on heat flow. ACM Transactions on Graphics. https://doi.org/10.1145/2516971.2516977
125
+
126
+ For non-singular mean curvature flow please cite:
127
+
128
+ [4] Kazhdan M, Solomon J, Ben-Chen M. 2012. Can Mean-Curvature Flow be Modified to be Non-singular? Comput. Graph. Forum 31, 5, 1745–1754.
129
+ https://doi.org/10.1111/j.1467-8659.2012.03179.x
130
+
131
+ We also invite you to check out our lab webpage at https://deep-mi.org
132
+
133
+ Martin Reuter
lapy-0.3.0/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # LaPy
2
+
3
+ LaPy is a package to compute spectral features (Laplace-Beltrami operator) on
4
+ tetrahedral and triangle meshes. It is written purely in python 3 without
5
+ sacrificing speed as almost all loops are vectorized, drawing upon efficient
6
+ and sparse mesh data structures. It is basically a port of the C++ ShapeDNA
7
+ project with extended differential geometry capabilities.
8
+
9
+ ## Contents:
10
+
11
+ - TriaMesh: a class for triangle meshes offering various operations, such as
12
+ fixing orientation, smoothing, curvature, boundary, quality, normals, and
13
+ various efficient mesh datastructure (edges, adjacency matrices)
14
+ - TetMesh: a class for tetrahedral meshes (orientation, boundary ...)
15
+ - TriaIO, TetIO: for both tets and trias from off, vtk, etc. formats
16
+ - FuncIO: import/export vertex functions and eigenvector files
17
+ - Solver: a class for linear FEM computation (Laplace stiffness and mass
18
+ matrix, fast and sparse eigenvalue solver, anisotropic Laplace, Poisson)
19
+ - DiffGeo: compute gradients, divergence, mean curvature flow, etc.
20
+ - Heat: for heat kernel and diffusion
21
+ - ShapeDNA: compute the ShapeDNA descriptor of surfaces and solids
22
+ - Plot: functions for interactive visualization (wrapping plotly)
23
+
24
+ ## ToDo:
25
+
26
+ - Add unit tests and automated testing (e.g. travis)
27
+ - Add command line scripts for various functions
28
+
29
+ ## Usage:
30
+
31
+ The LaPy package is a comprehensive collection of scripts, so we refer to the
32
+ 'help' function and docstring of each module / function / class for usage info.
33
+ For example:
34
+
35
+ ```
36
+ import lapy as lp
37
+ help(lp.TriaMesh)
38
+ help(lp.Solver)
39
+ ```
40
+
41
+ In the `examples` subdirectory, we provide several Jupyter notebooks that
42
+ illustrate prototypical use cases of the toolbox.
43
+
44
+ ## Installation:
45
+
46
+ Use the following code to download, build and install a package from this
47
+ repository into your local Python package directory:
48
+
49
+ `pip3 install --user git+https://github.com/Deep-MI/LaPy.git#egg=lapy`
50
+
51
+ Use the following code to install the package in editable mode to a location of
52
+ your choice:
53
+
54
+ `pip3 install --user --src /my/preferred/location --editable git+https://github.com/Deep-MI/Lapy.git#egg=lapy`
55
+
56
+ Several functions, e.g. the Solver, require a sparse matrix decomposition, for which either the LU decomposition (from scipy sparse) or the faster Cholesky decomposition (from scikit-sparse cholmod) can be used. If the parameter flag use_cholmod is True, the code will try to import cholmod from the scikit-sparse package and will fall back to LU if the import fails. If you would like to use cholmod, you need to install scikit-sparse separately. It cannot be listed among LaPy's dependencies (e.g. in setup.py or requirements.txt) as that causes errors with pip. scikit-sparse requires numpy and scipy to be installed separately beforehand.
57
+
58
+ ## References:
59
+
60
+ If you use this software for a publication please cite both these papers:
61
+
62
+ [1] Laplace-Beltrami spectra as 'Shape-DNA' of surfaces and solids. Reuter M, Wolter F-E, Peinecke N Computer-Aided Design. 2006;38(4):342-366. http://dx.doi.org/10.1016/j.cad.2005.10.011
63
+
64
+ [2] BrainPrint: a discriminative characterization of brain morphology. Wachinger C, Golland P, Kremen W, Fischl B, Reuter M Neuroimage. 2015;109:232-48. http://dx.doi.org/10.1016/j.neuroimage.2015.01.032 http://www.ncbi.nlm.nih.gov/pubmed/25613439
65
+
66
+ [1] introduces the FEM methods and the Laplace spectra for shape analysis, while [2] focusses on medical applications.
67
+
68
+ For Geodesics please cite:
69
+
70
+ [3] Crane K, Weischedel C, Wardetzky M. Geodesics in heat: A new approach to computing distance based on heat flow. ACM Transactions on Graphics. https://doi.org/10.1145/2516971.2516977
71
+
72
+ For non-singular mean curvature flow please cite:
73
+
74
+ [4] Kazhdan M, Solomon J, Ben-Chen M. 2012. Can Mean-Curvature Flow be Modified to be Non-singular? Comput. Graph. Forum 31, 5, 1745–1754.
75
+ https://doi.org/10.1111/j.1467-8659.2012.03179.x
76
+
77
+ We also invite you to check out our lab webpage at https://deep-mi.org
78
+
79
+ Martin Reuter
@@ -0,0 +1,441 @@
1
+ # Adopted from Matlab code at
2
+ # https://github.com/garyptchoi/spherical-conformal-map
3
+ # with this
4
+ # Copyright (c) 2013-2020, Gary Pui-Tung Choi
5
+ # https://math.mit.edu/~ptchoi
6
+ # and has been distributed with the Apache 2 License
7
+
8
+ # If you use this code in your own work, please cite the following paper:
9
+ # [1] P. T. Choi, K. C. Lam, and L. M. Lui,
10
+ # "FLASH: Fast Landmark Aligned Spherical Harmonic Parameterization for Genus-0
11
+ # Closed Brain Surfaces."
12
+ # SIAM Journal on Imaging Sciences, vol. 8, no. 1, pp. 67-94, 2015.
13
+
14
+
15
+ import numpy as np
16
+ from scipy import sparse
17
+ from scipy.optimize import minimize
18
+
19
+ from .Solver import Solver
20
+ from .TriaMesh import TriaMesh
21
+ from .utils._imports import import_optional_dependency
22
+
23
+
24
+ def spherical_conformal_map(tria):
25
+ """
26
+ A linear method for computing spherical conformal map of a genus-0 closed surface
27
+
28
+ Input: TriaMesh (vertices and faces)
29
+ Output:
30
+ mapped_vertices: nv x 3 vertex coordinates of the spherical conformal parameterization
31
+
32
+ If you use this code in your own work, please cite the following paper:
33
+ [1] P. T. Choi, K. C. Lam, and L. M. Lui,
34
+ "FLASH: Fast Landmark Aligned Spherical Harmonic Parameterization for Genus-0 Closed Brain Surfaces."
35
+ SIAM Journal on Imaging Sciences, vol. 8, no. 1, pp. 67-94, 2015.
36
+
37
+ Adopted from Matlab code at
38
+ https://github.com/garyptchoi/spherical-conformal-map
39
+ with this
40
+ Copyright (c) 2013-2020, Gary Pui-Tung Choi
41
+ https://math.mit.edu/~ptchoi
42
+ and has been distributed with the Apache 2 License
43
+ """
44
+ # Check whether the input mesh is spherical topology (genus-0)
45
+ if tria.euler() != 2:
46
+ print("ERROR: The mesh is not a genus-0 closed surface ..")
47
+ raise ValueError("not genus-0")
48
+
49
+ # Find the most regular triangle as the "big triangle"
50
+ tquals = tria.tria_qualities()
51
+ bigtri = np.argmax(tquals)
52
+ # print(bigtri, tquals[bigtri])
53
+ # If it turns out that the spherical parameterization result is homogeneous
54
+ # you can try to change bigtri to the id of some other triangles with good quality
55
+
56
+ # North pole step: Compute spherical map by solving laplace equation on a big triangle
57
+ nv = tria.v.shape[0]
58
+ S = Solver(tria)
59
+ M = S.stiffness.astype(complex)
60
+
61
+ p0 = tria.t[bigtri, 0]
62
+ p1 = tria.t[bigtri, 1]
63
+ p2 = tria.t[bigtri, 2]
64
+ fixed = tria.t[bigtri, :]
65
+
66
+ # set all rows and cols with fixed vidxs to zero
67
+ # and set diag entries to 1
68
+ mrow, mcol, mval = sparse.find(M[fixed, :])
69
+ M = (
70
+ M
71
+ - sparse.csc_matrix((mval, (fixed[mrow], mcol)), shape=(nv, nv))
72
+ + sparse.csc_matrix(((1, 1, 1), (fixed, fixed)), shape=(nv, nv))
73
+ )
74
+
75
+ # find embedding of the bigtria (boundary condition later)
76
+ # arbitrarily set first two points
77
+ x0, y0, x1, y1 = 0, 0, 1, 0
78
+ a = tria.v[p1, :] - tria.v[p0, :]
79
+ b = tria.v[p2, :] - tria.v[p0, :]
80
+ sin1 = np.linalg.norm(np.cross(a, b)) / (np.linalg.norm(a) * np.linalg.norm(b))
81
+ ori_h = np.linalg.norm(b) * sin1
82
+ ratio = np.sqrt(((x0 - x1) ** 2 + (y0 - y1) ** 2)) / np.linalg.norm(a)
83
+ y2 = ori_h * ratio # compute the coordinates of the third vertex
84
+ x2 = np.sqrt(np.linalg.norm(b) ** 2 * ratio**2 - y2**2)
85
+ # should be around (0.5, sqrt(3)/2) if we found an equilateral bigtri
86
+
87
+ # Solve the Laplace equation to obtain a harmonic map
88
+ c = np.zeros((nv, 1))
89
+ c[p0], c[p1], c[p2] = x0, x1, x2
90
+ d = np.zeros((nv, 1))
91
+ d[p0], d[p1], d[p2] = y0, y1, y2
92
+ rhs = np.empty(c.shape[:-1], dtype=complex)
93
+ rhs.real = c.flatten()
94
+ rhs.imag = d.flatten()
95
+
96
+ z = sparse_symmetric_solve(M, rhs)
97
+ z = np.squeeze(np.array(z))
98
+ z = z - np.mean(z, axis=0)
99
+
100
+ # inverse stereographic projection (not scaled well)
101
+ S = inverse_stereographic(z)
102
+
103
+ # Find optimal big triangle size
104
+ w = np.empty(S.shape[:-1], dtype=complex)
105
+ w.real = (S[:, 0] / (1 + S[:, 2])).flatten()
106
+ w.imag = (S[:, 1] / (1 + S[:, 2])).flatten()
107
+
108
+ # find the index of the southernmost triangle
109
+ index = np.argsort(
110
+ np.abs(z[tria.t[:, 0]]) + np.abs(z[tria.t[:, 1]]) + np.abs(z[tria.t[:, 2]])
111
+ )
112
+ inner = index[0]
113
+ if inner == bigtri:
114
+ inner = index[1]
115
+
116
+ # Compute the size of the northern most and the southern most triangles
117
+ NorthTriSide = (
118
+ np.abs(z[tria.t[bigtri, 0]] - z[tria.t[bigtri, 1]])
119
+ + np.abs(z[tria.t[bigtri, 1]] - z[tria.t[bigtri, 2]])
120
+ + np.abs(z[tria.t[bigtri, 2]] - z[tria.t[bigtri, 0]])
121
+ ) / 3.0
122
+
123
+ SouthTriSide = (
124
+ np.abs(w[tria.t[inner, 0]] - w[tria.t[inner, 1]])
125
+ + np.abs(w[tria.t[inner, 1]] - w[tria.t[inner, 2]])
126
+ + np.abs(w[tria.t[inner, 2]] - w[tria.t[inner, 0]])
127
+ ) / 3.0
128
+
129
+ # rescale to get the best distribution
130
+ z = z * np.sqrt(NorthTriSide * SouthTriSide) / NorthTriSide
131
+
132
+ # inverse stereographic projection (now distributed well)
133
+ S = inverse_stereographic(z)
134
+
135
+ if np.isnan(np.sum(S)):
136
+ raise ValueError("Error: projection contains nan value(s)!")
137
+ # could revert to spherical tutte map here
138
+
139
+ # South pole step
140
+ idx = np.argsort(S[:, 2])
141
+
142
+ # number of points near the south pole to be fixed
143
+ # simply set it to be 1/10 of the total number of vertices (can be changed)
144
+ # In case the spherical parameterization is not good, change 10 to
145
+ # something smaller (e.g. 2)
146
+ fixnum = np.maximum(round(nv / 10), 3)
147
+ fixed = idx[0 : np.minimum(nv, fixnum)]
148
+
149
+ # south pole stereographic projection
150
+ P = np.column_stack(
151
+ (S[:, 0] / (1 + S[:, 2]), S[:, 1] / (1 + S[:, 2]), np.zeros(nv))
152
+ )
153
+
154
+ # compute the Beltrami coefficient (value per triangle)
155
+ triasouth = TriaMesh(P, tria.t)
156
+ mu = beltrami_coefficient(triasouth, tria.v)
157
+
158
+ # compose the map with another quasi-conformal map to cancel the distortion
159
+ mapping = linear_beltrami_solver(triasouth, mu, fixed, P[fixed, :])
160
+
161
+ if np.isnan(np.sum(mapping)):
162
+ # if the result has NaN entries, then most probably the number of
163
+ # boundary constraints is not large enough
164
+ # increase the number of boundary constrains and run again
165
+ print("South pole compsed map has nan value(s)!")
166
+ fixnum = fixnum * 5 # again, this number can be changed
167
+ fixed = idx[0 : np.minimum(nv, fixnum)]
168
+ mapping = linear_beltrami_solver(triasouth, mu, fixed, P[fixed, :])
169
+ if np.isnan(np.sum(mapping)):
170
+ mapping = P # use the old result
171
+
172
+ # inverse south pole stereographic projection
173
+ mapping = inverse_stereographic(mapping)
174
+ return mapping
175
+
176
+
177
+ def mobius_area_correction_spherical(tria, mapping):
178
+ """
179
+ Find an optimal Mobius transformation for reducing the area distortion of a spherical conformal parameterization
180
+ using the method in [1].
181
+
182
+ Input:
183
+ tria : TriaMesh (vertices, triangle) of genus-0 closed triangle mesh
184
+ mapping: nv x 3 vertex coordinates of the spherical conformal parameterization
185
+
186
+ Output:
187
+ map_mobius: nv x 3 vertex coordinates of the updated spherical conformal parameterization
188
+ x: the optimal parameters for the Mobius transformation, where
189
+ f(z) = \frac{az+b}{cz+d}
190
+ = ((x(1)+x(2)*1j)*z+(x(3)+x(4)*1j))/((x(5)+x(6)*1j)*z+(x(7)+x(8)*1j))
191
+
192
+ If you use this code in your own work, please cite the following paper:
193
+ [1] G. P. T. Choi, Y. Leung-Liu, X. Gu, and L. M. Lui,
194
+ "Parallelizable global conformal parameterization of simply-connected surfaces via partial welding."
195
+ SIAM Journal on Imaging Sciences, 2020.
196
+
197
+ Adopted by Martin Reuter from Matlab code at
198
+ https://github.com/garyptchoi/spherical-conformal-map
199
+ with this
200
+ Copyright (c) 2019-2020, Gary Pui-Tung Choi
201
+ https://scholar.harvard.edu/choi
202
+ and has been distributed with the Apache 2 License
203
+ """
204
+
205
+ # Compute the tria areas with normalization
206
+ area_t = tria.tria_areas()
207
+ area_t = area_t / area_t.sum()
208
+ # Project the sphere onto the plane
209
+ z = stereographic(mapping)
210
+
211
+ def area_map(xx):
212
+ v = inverse_stereographic(
213
+ ((xx[0] + xx[1] * 1j) * z + (xx[2] + xx[3] * 1j))
214
+ / ((xx[4] + xx[5] * 1j) * z + (xx[6] + xx[7] * 1j))
215
+ )
216
+ area_v = TriaMesh(v, tria.t).tria_areas()
217
+ return area_v / area_v.sum()
218
+
219
+ # objective function: mean(abs(log(area_map/area_t)))
220
+ def d_area(xx):
221
+ a = np.abs(np.log(area_map(xx) / area_t))
222
+ return (a[np.isfinite(a)]).mean()
223
+
224
+ # Optimization setup
225
+ x0 = np.array([1, 0, 0, 0, 0, 0, 1, 0]) # initial guess
226
+ # lower and upper bounds
227
+ bnds = (
228
+ (-100, 100),
229
+ (-100, 100),
230
+ (-100, 100),
231
+ (-100, 100),
232
+ (-100, 100),
233
+ (-100, 100),
234
+ (-100, 100),
235
+ (-100, 100),
236
+ )
237
+ # Optimization (may further supply gradients for better result, not yet implemented)
238
+ # options = optimoptions('fmincon','Display','iter');
239
+ # x = fmincon(d_area,x0,[],[],[],[],lb,ub,[],options);
240
+ options = {"disp": True}
241
+ result = minimize(d_area, x0, bounds=bnds, options=options)
242
+ x = result.x
243
+ # obtain the conformal parameterization with area distortion corrected
244
+ fz = ((x[0] + x[1] * 1j) * z + (x[2] + x[3] * 1j)) / (
245
+ (x[4] + x[5] * 1j) * z + (x[6] + x[7] * 1j)
246
+ )
247
+ map_mobius = inverse_stereographic(fz)
248
+ return map_mobius, result
249
+
250
+
251
+ def beltrami_coefficient(tria, mapping):
252
+ """
253
+ Compute the Beltrami coefficient of a mapping.
254
+ If you use this code in your own work, please cite the following paper:
255
+ [1] P. T. Choi, K. C. Lam, and L. M. Lui,
256
+ "FLASH: Fast Landmark Aligned Spherical Harmonic Parameterization for Genus-0 Closed Brain Surfaces."
257
+ SIAM Journal on Imaging Sciences, vol. 8, no. 1, pp. 67-94, 2015.
258
+
259
+ Adopted by Martin Reuter from Matlab code at
260
+ https://github.com/garyptchoi/spherical-conformal-map
261
+ with this
262
+ Copyright (c) 2013-2020, Gary Pui-Tung Choi
263
+ https://math.mit.edu/~ptchoi
264
+ and has been distributed with the Apache 2 License
265
+ """
266
+ # here we should be in the plane
267
+ if np.amax(tria.v[:, 2]) - np.amin(tria.v[:, 2]) > 0.001:
268
+ print("ERROR: mesh should be on complex plane ..")
269
+ raise ValueError("not planar")
270
+
271
+ # get 2d vetrices, edges and area
272
+ v0 = (tria.v[tria.t[:, 0], :])[:, :-1]
273
+ v1 = (tria.v[tria.t[:, 1], :])[:, :-1]
274
+ v2 = (tria.v[tria.t[:, 2], :])[:, :-1]
275
+ e0 = v2 - v1
276
+ e1 = v0 - v2
277
+ e2 = v1 - v0
278
+ # double areas
279
+ areas2 = np.cross(e0, e1) # returns z-component is length
280
+
281
+ # create tria,vertex matrices (summing area normalized edge coords)
282
+ nf = tria.t.shape[0]
283
+ tids = np.arange(nf)
284
+ i = np.column_stack((tids, tids, tids)).reshape(-1)
285
+ j = tria.t.reshape(-1)
286
+ datx = (
287
+ np.column_stack((e0[:, 1], e1[:, 1], e2[:, 1])) / areas2[:, np.newaxis]
288
+ ).reshape(-1)
289
+ daty = -(
290
+ np.column_stack((e0[:, 0], e1[:, 0], e2[:, 0])) / areas2[:, np.newaxis]
291
+ ).reshape(-1)
292
+ nv = tria.v.shape[0]
293
+ Dx = sparse.csr_matrix((datx, (i, j)), shape=(nf, nv))
294
+ Dy = sparse.csr_matrix((daty, (i, j)), shape=(nf, nv))
295
+
296
+ dXdu = Dx.dot(mapping[:, 0])
297
+ dXdv = Dy.dot(mapping[:, 0])
298
+ dYdu = Dx.dot(mapping[:, 1])
299
+ dYdv = Dy.dot(mapping[:, 1])
300
+ dZdu = Dx.dot(mapping[:, 2])
301
+ dZdv = Dy.dot(mapping[:, 2])
302
+
303
+ E = dXdu**2 + dYdu**2 + dZdu**2
304
+ G = dXdv**2 + dYdv**2 + dZdv**2
305
+ F = dXdu * dXdv + dYdu * dYdv + dZdu * dZdv
306
+ mu = (E - G + 2j * F) / (E + G + 2.0 * np.sqrt(E * G - F**2))
307
+
308
+ return mu
309
+
310
+
311
+ def linear_beltrami_solver(tria, mu, landmark, target):
312
+ """
313
+ Linear Beltrami solver
314
+ If you use this code in your own work, please cite the following paper:
315
+ [1] P. T. Choi, K. C. Lam, and L. M. Lui,
316
+ "FLASH: Fast Landmark Aligned Spherical Harmonic Parameterization for Genus-0 Closed Brain Surfaces."
317
+ SIAM Journal on Imaging Sciences, vol. 8, no. 1, pp. 67-94, 2015.
318
+
319
+ Adopted by Martin Reuter from Matlab code at
320
+ https://github.com/garyptchoi/spherical-conformal-map
321
+ with this
322
+ Copyright (c) 2013-2020, Gary Pui-Tung Choi
323
+ https://math.mit.edu/~ptchoi
324
+ and has been distributed with the Apache 2 License
325
+ """
326
+
327
+ # here we should be in the plane
328
+ if np.amax(tria.v[:, 2]) - np.amin(tria.v[:, 2]) > 0.001:
329
+ print("ERROR: mesh should be on complex plane ..")
330
+ raise ValueError("not planar")
331
+
332
+ af = (1.0 - 2 * np.real(mu) + np.abs(mu) ** 2) / (1.0 - np.abs(mu) ** 2)
333
+ bf = -2.0 * np.imag(mu) / (1.0 - np.abs(mu) ** 2)
334
+ gf = (1.0 + 2 * np.real(mu) + np.abs(mu) ** 2) / (1.0 - np.abs(mu) ** 2)
335
+
336
+ # get 2D vertices (drop 3rd dim)
337
+ t0 = tria.t[:, 0]
338
+ t1 = tria.t[:, 1]
339
+ t2 = tria.t[:, 2]
340
+ v0 = (tria.v[t0, :])[:, :-1]
341
+ v1 = (tria.v[t1, :])[:, :-1]
342
+ v2 = (tria.v[t2, :])[:, :-1]
343
+
344
+ uxv0 = v1[:, 1] - v2[:, 1]
345
+ uyv0 = v2[:, 0] - v1[:, 0]
346
+ uxv1 = v2[:, 1] - v0[:, 1]
347
+ uyv1 = v0[:, 0] - v2[:, 0]
348
+ uxv2 = v0[:, 1] - v1[:, 1]
349
+ uyv2 = v1[:, 0] - v0[:, 0]
350
+
351
+ c0 = np.sqrt(uxv0**2 + uyv0**2)
352
+ c1 = np.sqrt(uxv1**2 + uyv1**2)
353
+ c2 = np.sqrt(uxv2**2 + uyv2**2)
354
+ s = 0.5 * (c0 + c1 + c2)
355
+ area2 = 2 * np.sqrt(s * (s - c0) * (s - c1) * (s - c2))
356
+
357
+ v00 = (af * uxv0 * uxv0 + 2 * bf * uxv0 * uyv0 + gf * uyv0 * uyv0) / area2
358
+ v11 = (af * uxv1 * uxv1 + 2 * bf * uxv1 * uyv1 + gf * uyv1 * uyv1) / area2
359
+ v22 = (af * uxv2 * uxv2 + 2 * bf * uxv2 * uyv2 + gf * uyv2 * uyv2) / area2
360
+ v01 = (
361
+ af * uxv1 * uxv0 + bf * uxv1 * uyv0 + bf * uxv0 * uyv1 + gf * uyv1 * uyv0
362
+ ) / area2
363
+ v12 = (
364
+ af * uxv2 * uxv1 + bf * uxv2 * uyv1 + bf * uxv1 * uyv2 + gf * uyv2 * uyv1
365
+ ) / area2
366
+ v20 = (
367
+ af * uxv0 * uxv2 + bf * uxv0 * uyv2 + bf * uxv2 * uyv0 + gf * uyv0 * uyv2
368
+ ) / area2
369
+
370
+ # create symmetric A
371
+ i = np.column_stack((t0, t1, t2, t0, t1, t1, t2, t2, t0)).reshape(-1)
372
+ j = np.column_stack((t0, t1, t2, t1, t0, t2, t1, t0, t2)).reshape(-1)
373
+ dat = np.column_stack((v00, v11, v22, v01, v01, v12, v12, v20, v20)).reshape(-1)
374
+ nv = tria.v.shape[0]
375
+ A = sparse.csc_matrix((dat, (i, j)), shape=(nv, nv), dtype=complex)
376
+
377
+ # convert target to complex and set b vector
378
+ targetc = target[:, 0] + 1j * target[:, 1]
379
+ b = -A[:, landmark] * targetc
380
+ b[landmark] = targetc
381
+
382
+ # set all rows and columns in landmark to zero and put diag 1
383
+ mrow, mcol, mval = sparse.find(A[landmark, :])
384
+ Azero = sparse.csc_matrix((mval, (landmark[mrow], mcol)), shape=(nv, nv))
385
+ A = A - Azero
386
+ mrow, mcol, mval = sparse.find(A[:, landmark])
387
+ Azero = sparse.csc_matrix((mval, (mrow, landmark[mcol])), shape=(nv, nv))
388
+ Aones = sparse.csr_matrix(
389
+ (np.ones(landmark.shape[0]), (landmark, landmark)), shape=(nv, nv)
390
+ )
391
+ A = A - Azero + Aones
392
+ A.eliminate_zeros()
393
+
394
+ x = sparse_symmetric_solve(A, b)
395
+
396
+ mapping = np.squeeze(np.array(x))
397
+ mapping = np.column_stack((np.real(mapping), np.imag(mapping)))
398
+ return mapping
399
+
400
+
401
+ def sparse_symmetric_solve(A, b, use_cholmod=True):
402
+ sksparse = import_optional_dependency("sksparse", raise_error=use_cholmod)
403
+ if sksparse is not None:
404
+ print("Solver: Cholesky decomposition from scikit-sparse cholmod ...")
405
+ chol = sksparse.cholmod.cholesky(A)
406
+ x = chol(b)
407
+ else:
408
+ from scipy.sparse.linalg import splu
409
+
410
+ print("Solver: spsolve (LU decomposition) ...")
411
+ lu = splu(A)
412
+ x = lu.solve(b)
413
+ return x
414
+
415
+
416
+ def stereographic(u):
417
+ # Map sphere to complex plane
418
+ # u has three columns (x,y,z)
419
+ # return z as array of complex numbers
420
+ x = u[:, 0]
421
+ y = u[:, 1]
422
+ z = u[:, 2]
423
+ v = np.empty(u.shape[:-1], dtype=complex)
424
+ v.real = (x / (1 - z)).flatten()
425
+ v.imag = (y / (1 - z)).flatten()
426
+ return v
427
+
428
+
429
+ def inverse_stereographic(u):
430
+ # Computes mapping from complex plane to sphere
431
+ # u can be complex array, or two columns (real,img)
432
+ # returns v as (nv x 3) coordinates on sphere
433
+ if np.iscomplexobj(u):
434
+ x = u.real
435
+ y = u.imag
436
+ else:
437
+ x = u[:, 0]
438
+ y = u[:, 1]
439
+ z = 1 + x**2 + y**2
440
+ v = np.column_stack((2 * x / z, 2 * y / z, (-1 + x**2 + y**2) / z))
441
+ return v