-
Notifications
You must be signed in to change notification settings - Fork 53
/
Copy pathlaplace_beltrami.py
858 lines (721 loc) · 32.3 KB
/
laplace_beltrami.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
#!/usr/bin/python
"""
Compute the Laplace-Beltrami spectrum using a linear finite element method.
We follow the definitions and steps given in Reuter et al.'s 2009 paper:
"Discrete Laplace-Beltrami Operators for Shape Analysis and Segmentation"
References (please cite when using for publication):
Martin Reuter et al.
Discrete Laplace-Beltrami Operators for Shape Analysis and Segmentation
Computers & Graphics 33(3):381-390, 2009
Martin Reuter et al.
Laplace-Beltrami spectra as "Shape-DNA" of surfaces and solids
Computer-Aided Design 38(4):342-366, 2006
Dependency:
Scipy 0.10 or later to solve the generalized eigenvalue problem.
Information about using Scipy to solve a generalized eigenvalue problem:
http://docs.scipy.org/doc/scipy/reference/tutorial/arpack.html
NOTE ::
For "points," only include coordinates of vertices in the 3-D structure
whose spectrum is to be calculated. For example, do not use coordinates
of all POINTS from a VTK file as "points" and use only corresponding
faces as "faces." Otherwise this will cause a singular matrix error
when inverting matrices because some rows are all zeros.
Acknowledgments:
- Dr. Martin Reuter, Harvard Medical School, provided his MATLAB code, and
contributed code and bug fixes. He also assisted us in understanding his
articles about the Laplace-Beltrami operator.
- Dr. Eric You Xu, Google (http://www.youxu.info/),
explained how eigenvalue problems are solved numerically.
Authors:
- Martin Reuter, 2009-2016, http://reuter.mit.edu/
- Eliezer Stavsky, 2012 (eli.stavsky@gmail.com)
- Forrest Sheng Bao, 2012-2013 (forrest.bao@gmail.com) http://fsbao.net
- Arno Klein, 2012-2016 (arno@mindboggle.info) http://binarybottle.com
Copyright 2016, Mindboggle team (http://mindboggle.info), Apache v2.0 License
"""
def computeAB(points, faces):
"""
Compute matrices for the Laplace-Beltrami operator.
The matrices correspond to A and B from Reuter's 2009 article.
Note ::
All points must be on faces. Otherwise, a singular matrix error
is generated when inverting D.
Parameters
----------
points : list of lists of 3 floats
x,y,z coordinates for each vertex
faces : list of lists of 3 integers
each list contains indices to vertices that form a triangle on a mesh
Returns
-------
A : csr_matrix
B : csr_matrix
Examples
--------
>>> # Define a cube, then compute A and B on a selection of faces.
>>> import numpy as np
>>> from mindboggle.shapes.laplace_beltrami import computeAB
>>> points = [[0,0,0], [1,0,0], [0,0,1], [0,1,1],
... [1,0,1], [0,1,0], [1,1,1], [1,1,0]]
>>> points = np.array(points)
>>> faces = [[0,2,4], [0,1,4], [2,3,4], [3,4,5], [3,5,6], [0,1,7]]
>>> faces = np.array(faces)
>>> A, B = computeAB(points, faces)
>>> print(np.array_str(A.toarray(), precision=5, suppress_small=True))
[[ 1.5 -1. -0.5 0. 0. 0. 0. 0. ]
[-1. 2. 0. 0. -0.5 0. 0. -0.5 ]
[-0.5 0. 2. -0.5 -1. 0. 0. 0. ]
[ 0. 0. -0.5 2.56066 -0.35355 -1.20711 -0.5 0. ]
[ 0. -0.5 -1. -0.35355 1.85355 0. 0. 0. ]
[ 0. 0. 0. -1.20711 0. 1.20711 0. 0. ]
[ 0. 0. 0. -0.5 0. 0. 0.5 0. ]
[ 0. -0.5 0. 0. 0. 0. 0. 0.5 ]]
>>> print(np.array_str(B.toarray(), precision=5, suppress_small=True))
[[0.25 0.08333 0.04167 0. 0.08333 0. 0. 0.04167]
[0.08333 0.16667 0. 0. 0.04167 0. 0. 0.04167]
[0.04167 0. 0.16667 0.04167 0.08333 0. 0. 0. ]
[0. 0. 0.04167 0.28452 0.10059 0.10059 0.04167 0. ]
[0.08333 0.04167 0.08333 0.10059 0.36785 0.05893 0. 0. ]
[0. 0. 0. 0.10059 0.05893 0.20118 0.04167 0. ]
[0. 0. 0. 0.04167 0. 0.04167 0.08333 0. ]
[0.04167 0.04167 0. 0. 0. 0. 0. 0.08333]]
"""
import numpy as np
from scipy import sparse
points = np.array(points)
faces = np.array(faces)
nfaces = faces.shape[0]
# Linear local matrices on unit triangle:
tB = (np.ones((3,3)) + np.eye(3)) / 24.0
tA00 = np.array([[ 0.5,-0.5, 0.0],
[-0.5, 0.5, 0.0],
[ 0.0, 0.0, 0.0]])
tA11 = np.array([[ 0.5, 0.0,-0.5],
[ 0.0, 0.0, 0.0],
[-0.5, 0.0, 0.5]])
tA0110 = np.array([[ 1.0,-0.5,-0.5],
[-0.5, 0.0, 0.5],
[-0.5, 0.5, 0.0]])
# Replicate into third dimension for each triangle
# (for tB, 1st index is the 3rd index in MATLAB):
tB = np.array([np.tile(tB, (1, 1)) for i in range(nfaces)])
tA00 = np.array([np.tile(tA00, (1, 1)) for i in range(nfaces)])
tA11 = np.array([np.tile(tA11, (1, 1)) for i in range(nfaces)])
tA0110 = np.array([np.tile(tA0110,(1, 1)) for i in range(nfaces)])
# Compute vertex coordinates and a difference vector for each triangle:
v1 = points[faces[:, 0], :]
v2 = points[faces[:, 1], :]
v3 = points[faces[:, 2], :]
v2mv1 = v2 - v1
v3mv1 = v3 - v1
def reshape_and_repeat(A):
"""
For a given 1-D array A, run the MATLAB code below.
M = reshape(M,1,1,nfaces);
M = repmat(M,3,3);
Please note that a0 is a 3-D matrix, but the 3rd index in NumPy
is the 1st index in MATLAB. Fortunately, nfaces is the size of A.
"""
return np.array([np.ones((3,3))*x for x in A])
# Compute length^2 of v3mv1 for each triangle:
a0 = np.sum(v3mv1 * v3mv1, axis=1)
a0 = reshape_and_repeat(a0)
# Compute length^2 of v2mv1 for each triangle:
a1 = np.sum(v2mv1 * v2mv1, axis=1)
a1 = reshape_and_repeat(a1)
# Compute dot product (v2mv1*v3mv1) for each triangle:
a0110 = np.sum(v2mv1 * v3mv1, axis=1)
a0110 = reshape_and_repeat(a0110)
# Compute cross product and 2*vol for each triangle:
cr = np.cross(v2mv1,v3mv1)
vol = np.sqrt(np.sum(cr*cr, axis=1))
# zero vol will cause division by zero below, so set to small value:
vol_mean = 0.001*np.mean(vol)
vol = [vol_mean if x == 0 else x for x in vol]
vol = reshape_and_repeat(vol)
# Construct all local A and B matrices (guess: for each triangle):
localB = vol * tB
localA = (1.0/vol) * (a0*tA00 + a1*tA11 - a0110*tA0110)
# Construct row and col indices.
# (Note: J in numpy is I in MATLAB after flattening,
# because numpy is row-major while MATLAB is column-major.)
J = np.array([np.tile(x, (3,1)) for x in faces])
I = np.array([np.transpose(np.tile(x, (3,1))) for x in faces])
# Flatten arrays and swap I and J:
J_new = I.flatten()
I_new = J.flatten()
localA = localA.flatten()
localB = localB.flatten()
# Construct sparse matrix:
A = sparse.csr_matrix((localA, (I_new, J_new)))
B = sparse.csr_matrix((localB, (I_new, J_new)))
return A, B
def area_normalize(points, faces, spectrum):
"""
Normalize a spectrum using areas as suggested in Reuter et al. (2006)
Parameters
----------
points : list of lists of 3 floats
x,y,z coordinates for each vertex of the structure
faces : list of lists of 3 integers
3 indices to vertices that form a triangle on the mesh
spectrum : list of floats
LB spectrum of a given shape defined by _points_ and _faces_
Returns
-------
new_spectrum : list of floats
LB spectrum normalized by area
Examples
--------
>>> import numpy as np
>>> from mindboggle.shapes.laplace_beltrami import area_normalize
>>> from mindboggle.shapes.laplace_beltrami import fem_laplacian
>>> # Define a cube:
>>> points = [[0,0,0], [0,1,0], [1,1,0], [1,0,0],
... [0,0,1], [0,1,1], [1,1,1], [1,0,1]]
>>> faces = [[0,1,2], [2,3,0], [4,5,6], [6,7,4], [0,4,7], [7,3,0],
... [0,4,5], [5,1,0], [1,5,6], [6,2,1], [3,7,6], [6,2,3]]
>>> spectrum = fem_laplacian(points, faces, spectrum_size=3,
... normalization=None)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[4.58359, 4.8]
>>> new_spectrum = area_normalize(points, faces, spectrum)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in new_spectrum[1::]]
[27.50155, 28.8]
"""
from mindboggle.guts.mesh import area_of_faces
area = area_of_faces(points, faces)
total_area = sum(area)
new_spectrum = [x*total_area for x in spectrum]
return new_spectrum
def index_normalize(spectrum):
"""
Normalize a spectrum by division of index to account for linear increase of
Eigenvalue magnitude (Weyl's law in 2D) as suggested in Reuter et al. (2006)
and used in BrainPrint (Wachinger et al. 2015)
Parameters
----------
spectrum : list of floats
LB spectrum of a given shape
Returns
-------
new_spectrum : list of floats
Linearly re-weighted LB spectrum
Examples
--------
>>> import numpy as np
>>> from mindboggle.shapes.laplace_beltrami import area_normalize
>>> from mindboggle.shapes.laplace_beltrami import fem_laplacian
>>> # Define a cube:
>>> points = [[0,0,0], [0,1,0], [1,1,0], [1,0,0],
... [0,0,1], [0,1,1], [1,1,1], [1,0,1]]
>>> faces = [[0,1,2], [2,3,0], [4,5,6], [6,7,4], [0,4,7], [7,3,0],
... [0,4,5], [5,1,0], [1,5,6], [6,2,1], [3,7,6], [6,2,3]]
>>> spectrum = fem_laplacian(points, faces, spectrum_size=3,
... normalization=None)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[4.58359, 4.8]
>>> new_spectrum = index_normalize(spectrum)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in new_spectrum[1::]]
[4.58359, 2.4]
"""
# define index list of floats
idx = [float(i) for i in range(1,len(spectrum) + 1)]
# if first entry is zero, shift index
if (abs(spectrum[0]<1e-09)):
idx = [i-1 for i in idx]
idx[0] = 1.0
# divide each element by its index
new_spectrum = [x/i for x, i in zip(spectrum, idx)]
return new_spectrum
def wesd(EVAL1, EVAL2, Vol1, Vol2, show_error=False, N=3):
"""
Weighted Spectral Distance. See Konukoglu et al. (2012)
Note ::
At present, algorithm doesn't return normalized result.
It therefore doesn't require calculation of volume.
Parameters
----------
EVAL1 : numpy array of floats
LB spectrum from the 1st shape
EVAL2 : numpy array of floats
LB spectrum from the 2nd shape
Vol1: float
Volume (area for 2D) of the 1st shape
Vol2: float
Volume (area for 2D) of the 2nd shape
show_error: boolean
display the error of WESD using Eqs.(9) and (10) in Konukoglu (2012)?
default: false
N : integer
The length of spetrum used (N>=3, default: 3)
"""
# Martin Reuter: "a surface is a 2d manifold.
# It doesn't matter that it is usually embedded in 3d Euclidean space."
d = 2.0
Ball = 4.0 / 3 * np.pi # For three dimensions
p = 2.0
Vol = np.amax((Vol1, Vol2))
mu = np.amax(EVAL1[1], EVAL2[1])
C = ((d+2)/(d*4*np.pi**2)*(Ball*Vol)**(2/d) - 1/mu)**p + \
((d+2)/(d*4*np.pi**2)*(Ball*Vol/2)**(2/d) - 1/mu*(d/(d+4)))**p
K = ((d+2)/(d*4*np.pi**2)*(Ball*Vol)**(2/d) - (1/mu)*(d/(d+2.64)))**p
# the right-hand side of Eq.(8) or the equation right after Eq.(4):
W = (C + K*(zeta(2*p/d,1) - 1 - .5**(2*p/d)))**(1/p)
holder = 0
for i in range(1, np.amin((len(EVAL1), len(EVAL2) )) ):
holder += (np.abs(EVAL1[i] - EVAL2[i])/(EVAL1[i]*EVAL2[i]))**p
WESD = holder ** (1/p)
#nWESD = WESD/W
if show_error:
WN = (C + K * (sum([n**(-1*2*p/d) for n in range(3,N+1)])))**(1/p)
# the second term on the right-hand side of Eq.(9)
#print("Truncation error of WESD is: {0}".format(W - WN))
#print("Truncation error of nWESD is: {1}".format(1 - WN/W))
return WESD
def fem_laplacian(points, faces, spectrum_size=10, normalization="areaindex",
verbose=False):
"""
Compute linear finite-element method Laplace-Beltrami spectrum
after Martin Reuter's MATLAB code.
Comparison of fem_laplacian() with Martin Reuter's Matlab eigenvalues:
fem_laplacian() results for Twins-2-1 left hemisphere (6 values):
[4.829758648026221e-18,
0.0001284173002467199,
0.0002715181572272745,
0.0003205150847159417,
0.0004701628070486448,
0.0005768904023010318]
Martin Reuter's shapeDNA-tria Matlab code:
{-4.7207711983791511358e-18 ;
0.00012841730024672144738 ;
0.00027151815722727096853 ;
0.00032051508471592313632 ;
0.0004701628070486902353 ;
0.00057689040230097490998 }
fem_laplacian() results for Twins-2-1 left postcentral (1022):
[6.3469513010430304e-18,
0.0005178862383467463,
0.0017434911095630772,
0.003667561767487686,
0.005429017880363784,
0.006309346984678924]
Martin Reuter's Matlab code:
{-2.1954862991027e-18 ;
0.0005178862383468 ;
0.0017434911095628 ;
0.0036675617674875 ;
0.0054290178803611 ;
0.006309346984678 }
Julien Lefevre, regarding comparison with Spongy results:
"I have done some comparisons between my Matlab codes and yours
on python and everything sounds perfect:
The evaluation has been done only for one mesh (about 10000 vertices).
- L2 error between your A and B matrices and mine are about 1e-16.
- I have also compared eigenvalues of the generalized problem;
even if the error is slightly increasing, it remains on the order
of machine precision.
- computation time for 1000 eigenvalues was 67s with python
versus 63s in matlab. And it is quite the same behavior for lower orders.
- Since the eigenvalues are increasing with order,
it is also interesting to look at the relative error...
high frequencies are not so much perturbed."
Parameters
----------
points : list of lists of 3 floats
x,y,z coordinates for each vertex of the structure
faces : list of lists of 3 integers
3 indices to vertices that form a triangle on the mesh
spectrum_size : integer
number of eigenvalues to be computed (the length of the spectrum)
normalization : string
the method used to normalize eigenvalues
if None, no normalization is used
if "area", use area of the 2D structure as in Reuter et al. 2006
if "index", divide eigenvalue by index to account for linear trend
if "areaindex", do both (default)
verbose : bool
print statements?
Returns
-------
spectrum : list
first spectrum_size eigenvalues for Laplace-Beltrami spectrum
Examples
--------
>>> import numpy as np
>>> from mindboggle.shapes.laplace_beltrami import fem_laplacian
>>> # Define a cube:
>>> points = [[0,0,0], [0,1,0], [1,1,0], [1,0,0],
... [0,0,1], [0,1,1], [1,1,1], [1,0,1]]
>>> faces = [[0,1,2], [2,3,0], [4,5,6], [6,7,4], [0,4,7], [7,3,0],
... [0,4,5], [5,1,0], [1,5,6], [6,2,1], [3,7,6], [6,2,3]]
>>> spectrum = fem_laplacian(points, faces, spectrum_size=3,
... normalization=None, verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[4.58359, 4.8]
>>> spectrum = fem_laplacian(points, faces, spectrum_size=3,
... normalization="area", verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[27.50155, 28.8]
>>> # Spectrum for entire left hemisphere of Twins-2-1:
>>> from mindboggle.mio.vtks import read_vtk
>>> from mindboggle.mio.fetch_data import prep_tests
>>> urls, fetch_data = prep_tests()
>>> label_file = fetch_data(urls['left_freesurfer_labels'], '', '.vtk')
>>> points, f1,f2, faces, labels, f3,f4,f5 = read_vtk(label_file)
>>> spectrum = fem_laplacian(points, faces, spectrum_size=6,
... normalization=None, verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[0.00013, 0.00027, 0.00032, 0.00047, 0.00058]
>>> # Spectrum for Twins-2-1 left postcentral pial surface (22):
>>> from mindboggle.guts.mesh import keep_faces, reindex_faces_points
>>> I22 = [i for i,x in enumerate(labels) if x==1022] # postcentral
>>> faces = keep_faces(faces, I22)
>>> faces, points, o1 = reindex_faces_points(faces, points)
>>> spectrum = fem_laplacian(points, faces, spectrum_size=6,
... normalization=None, verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[0.00057, 0.00189, 0.00432, 0.00691, 0.00775]
>>> # Area-normalized spectrum for a single label (postcentral):
>>> spectrum = fem_laplacian(points, faces, spectrum_size=6,
... normalization="area", verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[2.69259, 8.97865, 20.44857, 32.74477, 36.739]
"""
from scipy.sparse.linalg import eigsh, lobpcg
import numpy as np
from mindboggle.shapes.laplace_beltrami import computeAB
# ----------------------------------------------------------------
# Compute A and B matrices (from Reuter et al., 2009):
# ----------------------------------------------------------------
A, B = computeAB(points, faces)
if A.shape[0] <= spectrum_size:
if verbose:
print("The 3D shape has too few vertices ({0} <= {1}). Skip.".
format(A.shape[0], spectrum_size))
return None
# ----------------------------------------------------------------
# Use the eigsh eigensolver:
# ----------------------------------------------------------------
try :
# eigs is for nonsymmetric matrices while
# eigsh is for real-symmetric or complex-Hermitian matrices:
# Martin Reuter: "small sigma shift helps prevent numerical
# instabilities with zero eigenvalue"
eigenvalues, eigenvectors = eigsh(A, k=spectrum_size, M=B,
sigma=-0.01)
spectrum = eigenvalues.tolist()
# ----------------------------------------------------------------
# Use the lobpcg eigensolver:
# ----------------------------------------------------------------
except RuntimeError:
if verbose:
print("eigsh() failed. Now try lobpcg.")
print("Warning: lobpcg can produce different results from "
"Reuter (2006) shapeDNA-tria software.")
# Initial eigenvector values:
init_eigenvecs = np.random.random((A.shape[0], spectrum_size))
# maxiter = 40 forces lobpcg to use 20 iterations.
# Strangely, largest=false finds largest eigenvalues
# and largest=True gives the smallest eigenvalues:
eigenvalues, eigenvectors = lobpcg(A, init_eigenvecs, B=B,
largest=True, maxiter=40)
# Extract the real parts:
spectrum = [value.real for value in eigenvalues]
# For some reason, the eigenvalues from lobpcg are not sorted:
spectrum.sort()
# ----------------------------------------------------------------
# Normalize by area:
# ----------------------------------------------------------------
if normalization == "area":
spectrum = area_normalize(points, faces, spectrum)
if verbose:
print("Compute area-normalized linear FEM Laplace-Beltrami "
"spectrum")
elif normalization == "index":
spectrum = index_normalize(spectrum)
if verbose:
print("Compute index-normalized linear FEM Laplace-Beltrami"
" spectrum")
elif normalization == "areaindex":
spectrum = index_normalize(spectrum)
spectrum = area_normalize(points,faces,spectrum)
if verbose:
print("Compute area and index-normalized linear FEM "
"Laplace-Beltrami spectrum")
else:
if verbose:
print("Compute linear FEM Laplace-Beltrami spectrum")
return spectrum
def spectrum_of_largest(points, faces, spectrum_size=10, exclude_labels=[-1],
normalization="areaindex", areas=None, verbose=False):
"""
Compute Laplace-Beltrami spectrum on largest connected segment.
In case a surface patch is fragmented, we select the largest fragment,
remove extraneous triangular faces, and reindex indices.
Parameters
----------
points : list of lists of 3 floats
x,y,z coordinates for each vertex of the structure
faces : list of lists of 3 integers
3 indices to vertices that form a triangle on the mesh
spectrum_size : integer
number of eigenvalues to be computed (the length of the spectrum)
exclude_labels : list of integers
background values to exclude
normalization : string
the method used to normalize eigenvalues
if None, no normalization is used
if "area", use area of the 2D structure as in Reuter et al. 2006
if "index", divide eigenvalue by index to account for linear trend
if "areaindex", do both (default)
areas : numpy array or list of floats (or None)
surface area scalar values for all vertices
verbose : bool
print statements?
Returns
-------
spectrum : list
first spectrum_size eigenvalues for Laplace-Beltrami spectrum
Examples
--------
>>> # Spectrum for left postcentral + pars triangularis pial surfaces:
>>> import numpy as np
>>> from mindboggle.mio.vtks import read_scalars, read_vtk, write_vtk
>>> from mindboggle.guts.mesh import keep_faces, reindex_faces_points
>>> from mindboggle.shapes.laplace_beltrami import spectrum_of_largest
>>> from mindboggle.mio.fetch_data import prep_tests
>>> urls, fetch_data = prep_tests()
>>> label_file = fetch_data(urls['left_freesurfer_labels'], '', '.vtk')
>>> area_file = fetch_data(urls['left_area'], '', '.vtk')
>>> spectrum_size = 6
>>> exclude_labels = [-1]
>>> normalization = None
>>> points, indices, lines, faces, labels, f1, npoints, f2 = read_vtk(label_file,
... return_first=True, return_array=True)
>>> I20 = [i for i,x in enumerate(labels) if x==1020] # pars triangularis
>>> I22 = [i for i,x in enumerate(labels) if x==1022] # postcentral
>>> I22.extend(I20)
>>> faces = keep_faces(faces, I22)
>>> faces, points, o1 = reindex_faces_points(faces, points)
>>> areas, u1 = read_scalars(area_file, True, True)
>>> verbose = False
>>> spectrum = spectrum_of_largest(points, faces, spectrum_size,
... exclude_labels, normalization, areas, verbose)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[0.00057, 0.00189, 0.00432, 0.00691, 0.00775]
View both segments (skip test):
>>> from mindboggle.mio.plots import plot_surfaces
>>> scalars = np.zeros(np.shape(labels))
>>> scalars[I22] = 1
>>> vtk_file = 'test_two_labels.vtk'
>>> write_vtk(vtk_file, points, indices, lines, faces,
... scalars, scalar_names='scalars', scalar_type='int')
>>> plot_surfaces(vtk_file) # doctest: +SKIP
"""
import numpy as np
#from scipy.sparse.linalg import eigsh, lobpcg
from mindboggle.guts.segment import select_largest
from mindboggle.shapes.laplace_beltrami import fem_laplacian
if isinstance(areas, list):
areas = np.array(areas)
# Check to see if there are enough points:
min_points_faces = spectrum_size
npoints = len(points)
if npoints < min_points_faces or len(faces) < min_points_faces:
raise IOError("The input size {0} ({1} faces) should be much larger "
"than spectrum_size ({2})".
format(npoints, len(faces), spectrum_size))
return None
else:
# --------------------------------------------------------------------
# Select the largest segment (connected set of indices):
# --------------------------------------------------------------------
points, faces = select_largest(points, faces, exclude_labels, areas,
reindex=True)
# Alert if the number of indices is small:
if len(points) < min_points_faces:
raise IOError("The input size {0} is too small.".
format(len(points)))
return None
elif faces:
# ----------------------------------------------------------------
# Compute spectrum:
# ----------------------------------------------------------------
spectrum = fem_laplacian(points, faces, spectrum_size,
normalization, verbose)
return spectrum
else:
return None
def spectrum_from_file(vtk_file, spectrum_size=10, exclude_labels=[-1],
normalization="areaindex", area_file='', verbose=False):
"""
Compute Laplace-Beltrami spectrum of a 3D shape in a VTK file.
Parameters
----------
vtk_file : string
the input vtk file
spectrum_size : integer
number of eigenvalues to be computed (the length of the spectrum)
exclude_labels : list of integers
labels to be excluded
normalization : string
the method used to normalize eigenvalues
if None, no normalization is used
if "area", use area of the 2D structure as in Reuter et al. 2006
if "index", divide eigenvalue by index to account for linear trend
if "areaindex", do both (default)
area_file : string
name of VTK file with surface area scalar values
verbose : bool
print statements?
Returns
-------
spectrum : list of floats
first spectrum_size of Laplace-Beltrami spectrum
Examples
--------
>>> # Spectrum for entire left hemisphere of Twins-2-1:
>>> import numpy as np
>>> from mindboggle.shapes.laplace_beltrami import spectrum_from_file
>>> from mindboggle.shapes.laplace_beltrami import spectrum_per_label
>>> from mindboggle.mio.fetch_data import prep_tests
>>> urls, fetch_data = prep_tests()
>>> vtk_file = fetch_data(urls['left_freesurfer_labels'], '', '.vtk')
>>> spectrum = spectrum_from_file(vtk_file, spectrum_size=6,
... exclude_labels=[-1], normalization=None, area_file="", verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[0.00013, 0.00027, 0.00032, 0.00047, 0.00058]
>>> spectrum = spectrum_from_file(vtk_file, spectrum_size=6,
... exclude_labels=[-1], normalization="areaindex", area_file="",
... verbose=False)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum[1::]]
[14.12801, 14.93573, 11.75397, 12.93141, 12.69348]
"""
from mindboggle.mio.vtks import read_vtk, read_scalars
from mindboggle.shapes.laplace_beltrami import spectrum_of_largest
points, indices, lines, faces, scalars, scalar_names, npoints, \
input_vtk = read_vtk(vtk_file)
# Area file:
if area_file:
areas, u1 = read_scalars(area_file)
else:
areas = None
spectrum = spectrum_of_largest(points, faces, spectrum_size,
exclude_labels, normalization, areas,
verbose)
return spectrum
def spectrum_per_label(vtk_file, spectrum_size=10, exclude_labels=[-1],
normalization='areaindex', area_file='',
largest_segment=True, verbose=False):
"""
Compute Laplace-Beltrami spectrum per labeled region in a file.
Parameters
----------
vtk_file : string
name of VTK surface mesh file containing index scalars (labels)
spectrum_size : integer
number of eigenvalues to be computed (the length of the spectrum)
exclude_labels : list of integers
labels to be excluded
normalization : string
the method used to normalize eigenvalues
if None, no normalization is used
if "area", use area of the 2D structure as in Reuter et al. 2006
if "index", divide eigenvalue by index to account for linear trend
if "areaindex", do both (default)
area_file : string (optional)
name of VTK file with surface area scalar values
largest_segment : bool
compute spectrum only for largest segment with a given label?
verbose : bool
print statements?
Returns
-------
spectrum_lists : list of lists
first eigenvalues for each label's Laplace-Beltrami spectrum
label_list : list of integers
list of unique labels for which spectra are obtained
Examples
--------
>>> # Uncomment "if label==22:" below to run example:
>>> # Spectrum for Twins-2-1 left postcentral (22) pial surface:
>>> import numpy as np
>>> from mindboggle.shapes.laplace_beltrami import spectrum_per_label
>>> from mindboggle.mio.fetch_data import prep_tests
>>> urls, fetch_data = prep_tests()
>>> vtk_file = fetch_data(urls['left_freesurfer_labels'], '', '.vtk')
>>> area_file = fetch_data(urls['left_area'], '', '.vtk')
>>> spectrum_size = 6
>>> exclude_labels = [0] #[-1]
>>> largest_segment = True
>>> verbose = False
>>> spectrum_lists, label_list = spectrum_per_label(vtk_file,
... spectrum_size, exclude_labels, None, area_file, largest_segment,
... verbose)
>>> [np.float("{0:.{1}f}".format(x, 5)) for x in spectrum_lists[0][1::]]
[0.00054, 0.00244, 0.00291, 0.00456, 0.00575]
>>> label_list[0:10]
[1029, 1005, 1011, 1021, 1008, 1025, 999, 1013, 1007, 1022]
"""
from mindboggle.mio.vtks import read_vtk, read_scalars
from mindboggle.guts.mesh import keep_faces, reindex_faces_points
from mindboggle.shapes.laplace_beltrami import fem_laplacian,\
spectrum_of_largest
# Read VTK surface mesh file:
points, indices, lines, faces, labels, scalar_names, npoints, \
input_vtk = read_vtk(vtk_file)
# Area file:
if area_file:
areas, u1 = read_scalars(area_file)
else:
areas = None
# Loop through labeled regions:
ulabels = []
[ulabels.append(int(x)) for x in labels if x not in ulabels
if x not in exclude_labels]
label_list = []
spectrum_lists = []
for label in ulabels:
#if label == 22:
# print("DEBUG: COMPUTE FOR ONLY ONE LABEL")
# Determine the indices per label:
Ilabel = [i for i,x in enumerate(labels) if x == label]
if verbose:
print('{0} vertices for label {1}'.format(len(Ilabel), label))
# Remove background faces:
pick_faces = keep_faces(faces, Ilabel)
pick_faces, pick_points, o1 = reindex_faces_points(pick_faces, points)
# Compute Laplace-Beltrami spectrum for the label:
if largest_segment:
exclude_labels_inner = [-1]
spectrum = spectrum_of_largest(pick_points, pick_faces,
spectrum_size,
exclude_labels_inner,
normalization, areas, verbose)
else:
spectrum = fem_laplacian(pick_points, pick_faces, spectrum_size,
normalization, verbose)
# Append to a list of lists of spectra:
spectrum_lists.append(spectrum)
label_list.append(label)
return spectrum_lists, label_list
# ============================================================================
# Doctests
# ============================================================================
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=True) # py.test --doctest-modules
#if __name__ == "__main__":
# import numpy as np
# # You should get different outputs if you change the coordinates of points.
# # If you do NOT see changes, you may be computing the graph Laplacian.
#
# # Define a cube:
# points = [[0,0,0], [1,0,0], [0,0,1], [0,1,1],
# [1,0,1], [0,1,0], [1,1,1], [1,1,0]]
# # Pick some faces:
# faces = [[0,2,4], [0,1,4], [2,3,4], [3,4,5], [3,5,6], [0,1,7]]
#
# print("Linear FEM Laplace-Beltrami spectrum\n\t{0}\n".format(
# fem_laplacian(points, faces, spectrum_size=5)))