import numpy as np def psign(p1, p2, p3): return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); def PointInTriangle(pt, v1, v2, v3): d1 = psign(pt, v1, v2) d2 = psign(pt, v2, v3) d3 = psign(pt, v3, v1) has_neg = (d1 < 0) | (d2 < 0) | (d3 < 0) has_pos = (d1 > 0) | (d2 > 0) | (d3 > 0) return not (has_neg & has_pos) def get_edges(tri): return [(tri[0], tri[1]), (tri[0], tri[2]), (tri[1], tri[2])] def default_add(d, e, V, ti): if e in d: d[e][1].append(ti) else: a, b = e length = np.linalg.norm(V[a] - V[b]) if a == b: raise ValueError('Adding edge with same index:', e, ti) d[e] = [length, [ti]] return d def get_all_edges(V, T): E_dict = {} for n, tri in enumerate(T): E = get_edges(tri) # outputs list of edges with sorted vertex indices. eg [(4,7),(4,8),(7,8)] for e in E: a, b = e length = np.linalg.norm(V[a] - V[b]) E_dict = default_add(E_dict, e, V, n) return E_dict def split_long_edges(V, T): E_dict = get_all_edges(V, T) target_length = np.median([length for length, _ in E_dict.values()]) run = True n_init_V = len(V) while run: run = False max_length = 0 for cur_e in E_dict.keys(): cur_length, cur_Ti = E_dict[cur_e] if cur_length > max_length: max_length = cur_length e = cur_e Ti = cur_Ti a, b = cur_e if max_length > target_length: run = True # make a new vertex Vnew = V[a] + V[b] N = 2.0 Vnew = Vnew / N new_length = np.linalg.norm(Vnew - V[a]) n = len(V) V = np.concatenate([V, [Vnew]], axis=0) for ti in sorted(Ti)[::-1]: # find the vertex index which a,b is not co-linear with m = [i for i in T[ti] if i != a and i != b][0] old = np.pad(np.array([V[ii] for ii in T[ti]]), ((1,0), (0,0)), mode='wrap')+1 new1 = np.pad(np.array([V[ii] for ii in sorted([m, n, a])]), ((1,0), (0,0)), mode='wrap')+2 new2 = np.pad(np.array([V[ii] for ii in sorted([m, n, b])]), ((1,0), (0,0)), mode='wrap') T[ti] = sorted([m, n, a]) # ADD m,n,a REM a,b,m tj = len(T) T.append(sorted([m, n, b])) # ADD m,n,b # add new edges an = tuple(sorted([n, a])) am = tuple(sorted([m, a])) bn = tuple(sorted([n, b])) bm = tuple(sorted([m, b])) mn = tuple(sorted([m, n])) E_dict[tuple(sorted([m, b]))][1] = [tk for tk in E_dict[tuple(sorted([m, b]))][1] if tk != ti] cross_length = np.linalg.norm(V[m] - V[n]) E_dict = default_add(E_dict, an, V, ti) # ADD ti to an E_dict = default_add(E_dict, bn, V, tj) # ADD tj to bn E_dict = default_add(E_dict, bm, V, tj) # ADD tj to bm E_dict = default_add(E_dict, mn, V, ti) # ADD ti to mn E_dict = default_add(E_dict, mn, V, tj) # ADD tj to mn del E_dict[e] interior_vertex_indices = set(range(len(V))) for e in E_dict.keys(): if len(E_dict[e][1]) == 1: interior_vertex_indices = interior_vertex_indices - set(e) # Smooth interior triangulated points for _ in range(100): for i in interior_vertex_indices: Vs = [] for e in E_dict.keys(): if i in e: vn = [j for j in e if j != i][0] Vs.append(V[vn]) Vnew = np.mean(Vs, axis=0) V[i] = 0.7 * V[i] + 0.3 * Vnew return V, T def polygon_triangulate(P): N = len(P) P3 = np.pad(P, ((0,0), (0,1))) angle_sum = 0 angles = [] exterior_angles = [] T = [] for p1, p2, p3 in zip(P3, np.roll(P3, -1, axis=0), np.roll(P3, -2, axis=0)): v1 = p2 - p1 v2 = p3 - p2 cross = np.cross(v1, v2) dot = np.clip(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)), -1, 1) angle = np.sign(cross[2])*np.arccos(dot) exterior_angles.append(angle/(2*np.pi)*360) indices = np.arange(N) if np.sum(exterior_angles) < 0: indices = indices[::-1] # iteratively add a trinagle and remove the index to the point which can no longer be part of another triangle while len(indices) > 2: best_i = 0 best_j = 0 best_k = 0 best_n = 0 best_length = np.inf # iterate over the remaining points to find the best candidate for n, (i,j,k), in enumerate(zip(np.roll(indices, 1), indices, np.roll(indices, -1))): v1 = P3[j] - P3[i] v2 = P3[k] - P3[j] cross = np.cross(v1, v2) angle = np.arcsin(cross[2] / (np.linalg.norm(v1) * np.linalg.norm(v2))) length = np.linalg.norm(v1) + np.linalg.norm(v2) # pick the shortest triangle which is interior if angle > 0 and length < best_length: # check all point to see if one lies inside the triangle, skip in that case contains_point = False for nn in range(N): if nn == i or nn == j or nn == k: continue if PointInTriangle(P[nn], P[i], P[j], P[k]): contains_point = True break if contains_point: continue best_length = length best_i = i best_j = j best_k = k best_n = n # add triangle and remove index to point which can no longer be part of another triangle T.append(sorted([best_i, best_j, best_k])) indices = np.delete(indices, best_n) P, T = split_long_edges(P, T) return exterior_angles, P, T class PlotTriangulation: colors = np.random.uniform(0.2,1,(2048, 3)) def __init__(self, V, T, add_labels=True): import matplotlib.pyplot as plt V0 = np.copy(V) if np.argmax(np.max(V0, axis=0)) == 1: V0 = V0[:,::-1] margins = (np.max(V0, axis=0) - np.min(V0, axis=0))*0.05 V0 = V0 - np.min(V0, axis=0) + margins I = np.zeros((np.max(V0[:,1] + 1 + margins[1]).astype(np.int32), np.max(V0[:,0] + 1 + margins[0]).astype(np.int32))) plt.imshow(I, cmap='gray') plt.axis('off') plt.subplots_adjust(bottom=0, top=1, left=0,right=1) for j in range(len(T)): tri = T[j] t1 = plt.Polygon(V0[tri,:], alpha=0.7, color=PlotTriangulation.colors[j%2048]) plt.gca().add_patch(t1) if add_labels: plt.text(np.mean(V0[tri,0]), np.mean(V0[tri,1]), str(j)) if add_labels: for i, v in enumerate(V0): plt.text(v[0], v[1], str(i), color='orange')