lapy 0.3.0__py3-none-any.whl

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/Conformal.py ADDED
@@ -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