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 +21 -0
- lapy-0.3.0/PKG-INFO +133 -0
- lapy-0.3.0/README.md +79 -0
- lapy-0.3.0/lapy/Conformal.py +441 -0
- lapy-0.3.0/lapy/DiffGeo.py +613 -0
- lapy-0.3.0/lapy/FuncIO.py +295 -0
- lapy-0.3.0/lapy/Heat.py +92 -0
- lapy-0.3.0/lapy/Plot.py +570 -0
- lapy-0.3.0/lapy/ShapeDNA.py +139 -0
- lapy-0.3.0/lapy/Solver.py +776 -0
- lapy-0.3.0/lapy/TetIO.py +209 -0
- lapy-0.3.0/lapy/TetMesh.py +231 -0
- lapy-0.3.0/lapy/TriaIO.py +470 -0
- lapy-0.3.0/lapy/TriaMesh.py +890 -0
- lapy-0.3.0/lapy/__init__.py +5 -0
- lapy-0.3.0/lapy/_version.py +5 -0
- lapy-0.3.0/lapy/commands/__init__.py +0 -0
- lapy-0.3.0/lapy/commands/sys_info.py +18 -0
- lapy-0.3.0/lapy/read_geometry.py +162 -0
- lapy-0.3.0/lapy/utils/__init__.py +1 -0
- lapy-0.3.0/lapy/utils/_config.py +99 -0
- lapy-0.3.0/lapy/utils/_imports.py +57 -0
- lapy-0.3.0/lapy.egg-info/PKG-INFO +133 -0
- lapy-0.3.0/lapy.egg-info/SOURCES.txt +29 -0
- lapy-0.3.0/lapy.egg-info/dependency_links.txt +1 -0
- lapy-0.3.0/lapy.egg-info/entry_points.txt +2 -0
- lapy-0.3.0/lapy.egg-info/requires.txt +35 -0
- lapy-0.3.0/lapy.egg-info/top_level.txt +1 -0
- lapy-0.3.0/pyproject.toml +150 -0
- lapy-0.3.0/setup.cfg +4 -0
- lapy-0.3.0/setup.py +3 -0
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
|