Spectral clustering#

You can use Laplace spectra to cluster data. The idea is that the eigenvalues of the mesh’s Laplace spectrum can be embedded in a lower-dimensional space.

Data source for this notebooks: odedstein/meshes

import os
import glob
import napari_shape_odyssey as nso
import vedo
import numpy as np
import tqdm
from sklearn.manifold import TSNE
from sklearn.cluster import DBSCAN

import napari
import pandas as pd
import matplotlib.pyplot as plt
!git clone https://github.com/odedstein/meshes.git
Cloning into 'meshes'...
Updating files:  17% (17/96)
Updating files:  18% (18/96)
Updating files:  19% (19/96)
Updating files:  20% (20/96)
Updating files:  21% (21/96)
Updating files:  22% (22/96)
Updating files:  23% (23/96)
Updating files:  25% (24/96)
Updating files:  26% (25/96)
Updating files:  27% (26/96)
Updating files:  28% (27/96)
Updating files:  29% (28/96)
Updating files:  30% (29/96)
Updating files:  31% (30/96)
Updating files:  32% (31/96)
Updating files:  33% (32/96)
Updating files:  34% (33/96)
Updating files:  35% (34/96)
Updating files:  36% (35/96)
Updating files:  37% (36/96)
Updating files:  38% (37/96)
Updating files:  39% (38/96)
Updating files:  40% (39/96)
Updating files:  41% (40/96)
Updating files:  42% (41/96)
Updating files:  43% (42/96)
Updating files:  44% (43/96)
Updating files:  45% (44/96)
Updating files:  46% (45/96)
Updating files:  47% (46/96)
Updating files:  48% (47/96)
Updating files:  50% (48/96)
Updating files:  51% (49/96)
Updating files:  52% (50/96)
Updating files:  53% (51/96)
Updating files:  54% (52/96)
Updating files:  55% (53/96)
Updating files:  56% (54/96)
Updating files:  57% (55/96)
Updating files:  58% (56/96)
Updating files:  59% (57/96)
Updating files:  60% (58/96)
Updating files:  61% (59/96)
Updating files:  62% (60/96)
Updating files:  63% (61/96)
Updating files:  64% (62/96)
Updating files:  65% (63/96)
Updating files:  66% (64/96)
Updating files:  67% (65/96)
Updating files:  68% (66/96)
Updating files:  69% (67/96)
Updating files:  70% (68/96)
Updating files:  71% (69/96)
Updating files:  72% (70/96)
Updating files:  73% (71/96)
Updating files:  75% (72/96)
Updating files:  76% (73/96)
Updating files:  77% (74/96)
Updating files:  78% (75/96)
Updating files:  79% (76/96)
Updating files:  80% (77/96)
Updating files:  81% (78/96)
Updating files:  82% (79/96)
Updating files:  83% (80/96)
Updating files:  84% (81/96)
Updating files:  85% (82/96)
Updating files:  86% (83/96)
Updating files:  87% (84/96)
Updating files:  88% (85/96)
Updating files:  89% (86/96)
Updating files:  90% (87/96)
Updating files:  91% (88/96)
Updating files:  92% (89/96)
Updating files:  93% (90/96)
Updating files:  94% (91/96)
Updating files:  95% (92/96)
Updating files:  96% (93/96)
Updating files:  97% (94/96)
Updating files:  98% (95/96)
Updating files: 100% (96/96)
Updating files: 100% (96/96), done.
mesh_files = glob.glob(os.path.join('./meshes/**/*.obj'))
viewer = napari.Viewer()

Calculate spectra for every mesh#

eigenvalues = []
eigenvectors = []
meshes = []
for file in tqdm.tqdm(mesh_files):
    # load and preprocess meshes
    mesh = vedo.load(file).triangulate().clean().decimate(n=5000)
    bounding_box_volume = np.prod(mesh.bounds()[1] - mesh.bounds()[0])
    mesh.scale(1 / bounding_box_volume)

    mesh_tuple = (mesh.points(), np.asarray(mesh.faces()))
    meshes.append(mesh_tuple)
    viewer.add_surface(mesh_tuple, name = os.path.basename(file))
    _eigenvectors, _eigenvalues = nso.spectral.shape_fingerprint(mesh_tuple, order = 20)

    eigenvalues.append(_eigenvalues)
    eigenvectors.append(_eigenvectors)
# stack eigenvalues into dataframe
eigenvalues_stack = np.stack(eigenvalues)
df = pd.DataFrame(eigenvalues_stack, columns = [f'lambda_{i}' for i in range(eigenvalues_stack.shape[1])])
df.head()
lambda_0 lambda_1 lambda_2 lambda_3 lambda_4 lambda_5 lambda_6 lambda_7 lambda_8 lambda_9 lambda_10 lambda_11 lambda_12 lambda_13 lambda_14 lambda_15 lambda_16 lambda_17 lambda_18 lambda_19
0 -1.953646e-14 3.497411 4.534560 5.684497 9.985113 14.893090 22.529184 29.669639 30.541655 37.723114 41.284098 52.551030 58.486390 63.681724 64.850003 71.539644 78.696079 81.764765 95.658377 102.104163
1 1.382054e-14 3.497411 4.534560 5.684497 9.985113 14.893090 22.529184 29.669640 30.541655 37.723113 41.284097 52.551029 58.486391 63.681721 64.850001 71.539641 78.696077 81.764764 95.658375 102.104161
2 -2.596881e-15 0.683063 2.141403 4.065282 4.768848 4.828398 7.341017 7.398343 7.880799 10.123669 10.174560 11.785636 13.405222 14.186425 15.805213 16.056310 16.568611 18.689970 19.308144 21.437427
3 -3.480202e-14 7.361894 14.320784 29.855033 33.864331 38.218302 51.627914 55.100373 63.052792 71.963617 75.823578 91.888147 95.434760 99.987659 114.406247 123.437142 127.149471 140.136072 142.793974 153.672580
4 8.763823e-15 5.616643 7.917237 8.444330 16.360097 19.160159 20.802747 22.530132 30.035678 35.645911 37.769361 40.187326 41.239513 45.143890 48.583533 55.172578 60.318931 60.867925 64.006032 70.048105

Normalize spectra#

# Fit a linear curve to every row of the dataframe and divide every value in the row by the slope of the curve

slope = []
intercept = []
for i in range(0, len(df)):
    x = np.arange(0, len(df.columns))
    y = df.iloc[i].values
    m, b = np.polyfit(x, y, 1)
    slope.append(m)
    intercept.append(b)

df = df.div(slope, axis=0)

Dimensionality reduction#

embedder = TSNE(n_components=2)
X_embedded = embedder.fit_transform(df)

clusterer = DBSCAN(eps=0.25, min_samples=2)
clusterer.fit(X_embedded)

for idx, layer in enumerate(viewer.layers):
    mesh_tuple = list(layer.data)
    mesh_tuple[-1] *= clusterer.labels_[idx]
    viewer.layers[idx].data = tuple(mesh_tuple)
    viewer.layers[idx].colormap = 'tab10'
    viewer.layers[idx].contrast_limits = [-1, np.max(clusterer.labels_)]
plt.scatter(X_embedded[:, 0], X_embedded[:, 1], c = clusterer.labels_)
<matplotlib.collections.PathCollection at 0x155abe32bb0>
../../_images/09d6aff2574cf37dea3ec65a7cab8eaaa1e21bb6c6431b67a607488c20c2fcd9.png
for idx in np.argwhere(clusterer.labels_==4):
    viewer.add_surface(meshes[idx[0]], name = os.path.basename(mesh_files[idx[0]]))