diff --git a/polygon_triangulate.py b/polygon_triangulate.py new file mode 100644 index 0000000000000000000000000000000000000000..b42461cec82398ef1d9d720393b20506afdab808 --- /dev/null +++ b/polygon_triangulate.py @@ -0,0 +1,221 @@ +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')