-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecimation.py
More file actions
215 lines (162 loc) · 6.87 KB
/
decimation.py
File metadata and controls
215 lines (162 loc) · 6.87 KB
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
import time
import math
import heapq
import numpy as np
import scipy.sparse as sp
from psbody.mesh import Mesh
__author__ = ['rbonazzola', 'anuragranj']
'''
Part of this code was borrowed from the repository https://github.com/anuragranj/coma
'''
def row(A):
return A.reshape((1, -1))
def col(A):
return A.reshape((-1, 1))
def get_vert_connectivity(mesh_v, mesh_f):
"""Returns a sparse matrix (of size #verts x #verts) where each nonzero
element indicates a neighborhood relation. For example, if there is a
nonzero element in position (15,12), that means vertex 15 is connected
by an edge to vertex 12."""
vpv = sp.csc_matrix((len(mesh_v),len(mesh_v)))
# for each column in the faces...
for i in range(3):
IS = mesh_f[:,i]
JS = mesh_f[:,(i+1)%3]
data = np.ones(len(IS))
ij = np.vstack((row(IS.flatten()), row(JS.flatten())))
mtx = sp.csc_matrix((data, ij), shape=vpv.shape)
vpv = vpv + mtx + mtx.T
return vpv
def get_vertices_per_edge(mesh_v, mesh_f):
"""Returns an Ex2 array of adjacencies between vertices, where
each element in the array is a vertex index. Each edge is included
only once. If output of get_faces_per_edge is provided, this is used to
avoid call to get_vert_connectivity()"""
vc = sp.coo_matrix(get_vert_connectivity(mesh_v, mesh_f))
result = np.hstack((col(vc.row), col(vc.col)))
result = result[result[:,0] < result[:,1]] # for uniqueness
return result
def vertex_quadrics(mesh):
"""Computes a quadric for each vertex in the Mesh.
Returns:
v_quadrics: an (N x 4 x 4) array, where N is # vertices.
"""
# Allocate quadrics
v_quadrics = np.zeros((len(mesh.v), 4, 4,))
# For each face...
for f_idx in range(len(mesh.f)):
# Compute normalized plane equation for that face
vert_idxs = mesh.f[f_idx]
verts = np.hstack((mesh.v[vert_idxs], np.array([1, 1, 1]).reshape(-1, 1)))
u, s, v = np.linalg.svd(verts)
eq = v[-1, :].reshape(-1, 1)
eq = eq / (np.linalg.norm(eq[0:3]))
# Add the outer product of the plane equation to the
# quadrics of the vertices for this face
for k in range(3):
v_quadrics[mesh.f[f_idx, k], :, :] += np.outer(eq, eq)
return v_quadrics
def _get_sparse_transform(faces, num_original_verts):
verts_left = np.unique(faces.flatten())
IS = np.arange(len(verts_left))
JS = verts_left
data = np.ones(len(JS))
mp = np.arange(0, np.max(faces.flatten()) + 1)
mp[JS] = IS
new_faces = mp[faces.copy().flatten()].reshape((-1, 3))
ij = np.vstack((IS.flatten(), JS.flatten()))
mtx = sp.csc_matrix((data, ij), shape=(len(verts_left) , num_original_verts ))
return (new_faces, mtx)
def qslim_decimator_transformer(mesh, factor=None, n_verts_desired=None):
"""Returns a simplified version of this mesh, namely a tuple with (faces, downsampling_matrix).
A Qslim-style approach is used here.
:param factor: fraction of the original vertices to retain
:param n_verts_desired: number of the original vertices to retain
:returns: new_faces: An Fx3 array of faces, mtx: Transformation matrix
"""
if factor is None and n_verts_desired is None:
raise Exception('Need either factor or n_verts_desired.')
if n_verts_desired is None:
n_verts_desired = math.ceil(len(mesh.v) * factor)
Qv = vertex_quadrics(mesh)
# fill out a sparse matrix indicating vertex-vertex adjacency
# from psbody.mesh.topology.connectivity import get_vertices_per_edge
vert_adj = get_vertices_per_edge(mesh.v, mesh.f)
# vert_adj = sp.lil_matrix((len(mesh.v), len(mesh.v)))
# for f_idx in range(len(mesh.f)):
# vert_adj[mesh.f[f_idx], mesh.f[f_idx]] = 1
vert_adj = sp.csc_matrix((vert_adj[:, 0] * 0 + 1, (vert_adj[:, 0], vert_adj[:, 1])), shape=(len(mesh.v), len(mesh.v)))
vert_adj = vert_adj + vert_adj.T
vert_adj = vert_adj.tocoo()
def collapse_cost(Qv, r, c, v):
Qsum = Qv[r, :, :] + Qv[c, :, :]
p1 = np.vstack((v[r].reshape(-1, 1), np.array([1]).reshape(-1, 1)))
p2 = np.vstack((v[c].reshape(-1, 1), np.array([1]).reshape(-1, 1)))
destroy_c_cost = p1.T.dot(Qsum).dot(p1)
destroy_r_cost = p2.T.dot(Qsum).dot(p2)
result = {
'destroy_c_cost': destroy_c_cost,
'destroy_r_cost': destroy_r_cost,
'collapse_cost': min([destroy_c_cost, destroy_r_cost]),
'Qsum': Qsum}
return result
# construct a queue of edges with costs
queue = []
for k in range(vert_adj.nnz):
r = vert_adj.row[k]
c = vert_adj.col[k]
if r > c:
continue
cost = collapse_cost(Qv, r, c, mesh.v)['collapse_cost']
heapq.heappush(queue, (cost, (r, c)))
start = time.time()
# decimate
collapse_list = []
nverts_total = len(mesh.v)
faces = mesh.f.copy()
nverts_total_ = nverts_total
while nverts_total > n_verts_desired:
if nverts_total % 100 == 0 and nverts_total_ != nverts_total:
nverts_total_ = nverts_total
start = time.time()
e = heapq.heappop(queue)
r = e[1][0]
c = e[1][1]
if r == c:
continue
cost = collapse_cost(Qv, r, c, mesh.v)
if cost['collapse_cost'] > e[0]:
heapq.heappush(queue, (cost['collapse_cost'], e[1]))
continue
else:
# update old vert idxs to new one,
# in queue and in face list
if cost['destroy_c_cost'] < cost['destroy_r_cost']:
to_destroy = c
to_keep = r
else:
to_destroy = r
to_keep = c
collapse_list.append([to_keep, to_destroy])
# in our face array, replace "to_destroy" vertidx with "to_keep" vertidx
np.place(faces, faces == to_destroy, to_keep)
# same for queue
which1 = [idx for idx in range(len(queue)) if queue[idx][1][0] == to_destroy]
which2 = [idx for idx in range(len(queue)) if queue[idx][1][1] == to_destroy]
for k in which1:
queue[k] = (queue[k][0], (to_keep, queue[k][1][1]))
for k in which2:
queue[k] = (queue[k][0], (queue[k][1][0], to_keep))
Qv[r, :, :] = cost['Qsum']
Qv[c, :, :] = cost['Qsum']
a = faces[:, 0] == faces[:, 1]
b = faces[:, 1] == faces[:, 2]
c = faces[:, 2] == faces[:, 0]
# remove degenerate faces
def logical_or3(x, y, z):
return np.logical_or(x, np.logical_or(y, z))
faces_to_keep = np.logical_not(logical_or3(a, b, c))
faces = faces[faces_to_keep, :].copy()
nverts_total = (len(np.unique(faces.flatten())))
new_faces, mtx = _get_sparse_transform(faces, len(mesh.v))
return new_faces, mtx