feat: ca marche 1 PE

This commit is contained in:
gdamms 2022-10-17 15:51:37 +02:00
parent 0329b83aa6
commit 0c79065d11

394
main.py
View file

@ -1,10 +1,12 @@
import io import io
from types import NoneType from math import floor
import obja.obja as obja import obja.obja as obja
import numpy as np import numpy as np
import argparse import argparse
from time import time
from rich.progress import track, Progress
from rich.progress import track
def cot(x: float): def cot(x: float):
sin_x = np.sin(x) sin_x = np.sin(x)
@ -20,6 +22,55 @@ def sliding_window(l: list, n: int = 2):
return res return res
class Edge:
def __init__(self, a, b):
self.a = min(a, b)
self.b = max(a, b)
self.face1 = None
self.face2 = None
self.fold = 0.0
self.curvature = 0.0
def __eq__(self, __o: object) -> bool:
return self.a == __o.a and self.b == __o.b
class Face:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
self.normal = np.zeros(3)
def to_obja(self):
return obja.Face(self.a, self.b, self.c)
def __eq__(self, __o: object) -> bool:
if __o is None:
return False
return self.a == __o.a and self.b == __o.b and self.c == __o.c
class Vertex:
def __init__(self, pos):
self.pos = pos
self.vertex_ring = []
self.face_ring = []
self.normal = np.zeros(3)
self.area = 0.0
self.curvature = 0.0
def to_obja(self):
return self.pos
class MAPS(obja.Model): class MAPS(obja.Model):
"""_summary_ """_summary_
@ -29,7 +80,100 @@ class MAPS(obja.Model):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.deleted_faces = set()
def parse_file(self, path):
super().parse_file(path)
for i, vertex in enumerate(self.vertices):
self.vertices[i] = Vertex(vertex)
for i, face in enumerate(self.faces):
self.faces[i] = Face(face.a, face.b, face.c)
def update(self):
self.update_edges()
self.update_rings()
self.update_normals()
self.update_area_curvature()
def update_edges(self):
self.edges = {}
remaining_faces = self.faces.copy()
while None in remaining_faces:
remaining_faces.remove(None)
for face in track(self.faces, description='Update edges'):
if face is None:
continue
for a, b in sliding_window([face.a, face.b, face.c], n=2):
new_edge = Edge(a, b)
if self.edges.get(f"{new_edge.a}:{new_edge.b}") is None:
new_edge.face1 = face
if face in remaining_faces:
remaining_faces.remove(face)
for face2 in remaining_faces:
face2_vertices = (face2.a, face2.b, face2.c)
if not (a in face2_vertices and b in face2_vertices):
continue
new_edge.face2 = face2
break
if new_edge.face2 is None:
print('ooooooooooooooooooooooo')
self.edges[f"{new_edge.a}:{new_edge.b}"] = new_edge
def update_rings(self):
for i, vertex in enumerate(self.vertices):
if vertex is None:
continue
vertex_ring, face_ring = self.one_ring(i)
vertex.vertex_ring = vertex_ring
vertex.face_ring = face_ring
def update_area_curvature(self):
for i, vertex in enumerate(self.vertices):
if vertex is None:
continue
area, curvature = self.compute_area_curvature(i)
vertex.area = area
vertex.curvature = curvature
self.feature_edges = []
for edge in self.edges.values():
edge.fold = np.dot(edge.face1.normal, edge.face2.normal)
if edge.fold < 0.5:
self.feature_edges.append(edge)
def update_normals(self):
for face in self.faces:
if face is None:
continue
p1 = self.vertices[face.a].pos
p2 = self.vertices[face.b].pos
p3 = self.vertices[face.c].pos
u = p2 - p1
v = p3 - p1
n = np.cross(u, v)
n /= np.linalg.norm(n)
face.normal = n
self.vertices[face.a].normal += n
self.vertices[face.b].normal += n
self.vertices[face.c].normal += n
for vertex in self.vertices:
if vertex is None:
continue
norm = np.linalg.norm(vertex.normal)
if norm != 0:
vertex.normal /= norm
def one_ring(self, index: int) -> tuple[list[int], list[int]]: def one_ring(self, index: int) -> tuple[list[int], list[int]]:
""" Return the corresponding 1-ring """ Return the corresponding 1-ring
@ -40,11 +184,13 @@ class MAPS(obja.Model):
Returns: Returns:
list[int]: ordered list of the 1-ring vertices list[int]: ordered list of the 1-ring vertices
""" """
if self.vertices[index] is None:
return None, None
# Find the 1-ring faces # Find the 1-ring faces
ring_faces, ring_face_indices = [], [] ring_faces, ring_face_indices = [], []
for face_index, face in enumerate(self.faces): for face_index, face in enumerate(self.faces):
if face == None: if face is None:
continue continue
if index in (face.a, face.b, face.c): if index in (face.a, face.b, face.c):
@ -52,7 +198,9 @@ class MAPS(obja.Model):
ring_face_indices.append(face_index) ring_face_indices.append(face_index)
# Initialize the ring # Initialize the ring
start_index = ring_faces[0].a if ring_faces[0].a != index else ring_faces[0].b start_index = (ring_faces[0].a if ring_faces[0].a != index and ring_faces[0].c != index else
ring_faces[0].b if ring_faces[0].a != index and ring_faces[0].b != index else
ring_faces[0].c)
ring = [start_index] ring = [start_index]
ring_faces.pop(0) ring_faces.pop(0)
@ -79,7 +227,7 @@ class MAPS(obja.Model):
return ring, ring_face_indices return ring, ring_face_indices
def compute_area_curvature(self, index: int) -> tuple[float, float, int]: def compute_area_curvature(self, index: int) -> tuple[float, float]:
""" Compute area and curvature the corresponding 1-ring """ Compute area and curvature the corresponding 1-ring
Args: Args:
@ -88,15 +236,20 @@ class MAPS(obja.Model):
Returns: Returns:
tuple[float, float]: area and curvature tuple[float, float]: area and curvature
""" """
area_sum = 0 if self.vertices[index] is None:
laplace_sum = 0 return None, None
one_ring_vertices, _ = self.one_ring(index)
teta = 0.0 ring = self.vertices[index].vertex_ring
p1 = self.vertices[index] # the center of the one-ring p1 = self.vertices[index].pos
for index1, index2, index3 in sliding_window(one_ring_vertices, n=3): n1 = self.vertices[index].normal
p2 = self.vertices[index1] # the second vertice of the triangle
p3 = self.vertices[index2] # the third vertice of the triangle area_sum = 0
curvature = 0
for index1, index2 in sliding_window(ring, n=2):
# the second vertice of the triangle
p2 = self.vertices[index1].pos
p3 = self.vertices[index2].pos # the third vertice of the triangle
n2 = self.vertices[index1].normal
M = np.array([ # build the matrix, used to compute the area M = np.array([ # build the matrix, used to compute the area
[p1[0], p2[0], p3[0]], [p1[0], p2[0], p3[0]],
[p1[1], p2[1], p3[1]], [p1[1], p2[1], p3[1]],
@ -105,24 +258,17 @@ class MAPS(obja.Model):
area = abs(np.linalg.det(M) / 2) # compute the area area = abs(np.linalg.det(M) / 2) # compute the area
area_sum += area area_sum += area
teta += self.compute_angle(index1, index, index2) edge_curvature = np.dot(n2 - n1, p2 - p1) / \
np.linalg.norm(p2 - p1)**2
edge_curvature = abs(edge_curvature)
edge_key = f"{min(index, index1)}:{max(index, index1)}"
self.edges[edge_key].curvature = edge_curvature
laplace = self.compute_laplace(index, index1, index2, index3) curvature += edge_curvature
laplace_sum += laplace
K = (2 * np.pi - teta) / area * 3 curvature /= len(ring)
H = np.linalg.norm(laplace_sum) / 4 / area * 3
curvature = abs(H - np.sqrt(H*H - K)) + abs(H + np.sqrt(H*H - K))
curvature = K
return area_sum, curvature, len(one_ring_vertices) return area_sum, curvature
def compute_laplace(self, i: int, j: int, a: int, b: int) -> np.ndarray:
alpha = self.compute_angle(i, a, j)
beta = self.compute_angle(i, b, j)
cot_sum = cot(alpha) + cot(beta)
vec = self.vertices[j] - self.vertices[i]
return cot_sum * vec
def compute_priority(self, lamb: float = 0.0, max_length: int = 12) -> list[float]: def compute_priority(self, lamb: float = 0.0, max_length: int = 12) -> list[float]:
""" Compute selection priority of vertices (0.0 -> hight priority ; 1.0 -> low priority) """ Compute selection priority of vertices (0.0 -> hight priority ; 1.0 -> low priority)
@ -134,33 +280,25 @@ class MAPS(obja.Model):
Returns: Returns:
list[float]: priority values list[float]: priority values
""" """
# Compute area and curvature for each vertex max_area = max(
areas, curvatures, ring_lengths = [], [], [] [vertex.area for vertex in self.vertices if vertex is not None])
for i in range(len(self.vertices)): max_curvature = max(
if type(self.vertices[i]) != NoneType: [vertex.curvature for vertex in self.vertices if vertex is not None])
area, curvature, ring_length = self.compute_area_curvature(i)
else:
area, curvature, ring_length = -1.0, -1.0, None
areas.append(area)
curvatures.append(curvature)
ring_lengths.append(ring_length)
# Get maxes to normalize
max_area = max(areas)
max_curvature = max(curvatures)
# Compute priorities # Compute priorities
priorities = [] priorities = []
for a, k, l in zip(areas, curvatures, ring_lengths): for vertex in self.vertices:
if l != None and l <= max_length: if vertex is not None and len(vertex.vertex_ring) < max_length:
# Compute priority # Compute priority
priority = lamb * a / max_area + \ priority = (
(1.0 - lamb) * k / max_curvature lamb * vertex.area / max_area +
(1.0 - lamb) * vertex.curvature / max_curvature
)
else: else:
# Vertex with low priority # Vertex with low priority
priority = 2.0 priority = 2.0
priorities.append(priority) priorities.append(priority)
# return np.random.rand(len(priorities))
return priorities return priorities
def select_vertices(self) -> list[int]: def select_vertices(self) -> list[int]:
@ -169,25 +307,37 @@ class MAPS(obja.Model):
Returns: Returns:
list[int]: selected vertices list[int]: selected vertices
""" """
print("Selecting vertices...")
# Order vertices by priority # Order vertices by priority
priorities = self.compute_priority() priorities = self.compute_priority()
vertices = [i[0] vertices = [i[0]
for i in sorted(enumerate(priorities), key=lambda p: p[1])] for i in sorted(enumerate(priorities), key=lambda p: p[1])]
selected_vertices = [] selected_vertices = []
while len(vertices) > 0:
with Progress() as progress:
task = progress.add_task('Select vertices', total=len(vertices))
while not progress.finished:
# Select prefered vertex # Select prefered vertex
vertex = vertices.pop(0) # remove it from remaining vertices vertex = vertices.pop(0) # remove it from remaining vertices
progress.advance(task)
if priorities[vertex] == 2.0: if priorities[vertex] == 2.0:
continue continue
incident_count = 0
for feature_edge in self.feature_edges:
if vertex in (feature_edge.a, feature_edge.b):
incident_count += 1
if incident_count > 2:
continue
selected_vertices.append(vertex) selected_vertices.append(vertex)
# Remove neighbors # Remove neighbors
# for face in remaining_faces: # for face in remaining_faces:
for face in self.faces: for face in self.faces:
if face == None: if face is None:
continue continue
face_vertices = (face.a, face.b, face.c) face_vertices = (face.a, face.b, face.c)
@ -198,9 +348,9 @@ class MAPS(obja.Model):
for face_vertex in face_vertices: for face_vertex in face_vertices:
if face_vertex in vertices: if face_vertex in vertices:
vertices.remove(face_vertex) vertices.remove(face_vertex)
progress.advance(task)
print("Vertices selected.") return selected_vertices[:floor(1.0 * len(selected_vertices))]
return selected_vertices
def project_polar(self, index: int) -> list[np.ndarray]: def project_polar(self, index: int) -> list[np.ndarray]:
""" Flatten the 1-ring to retriangulate """ Flatten the 1-ring to retriangulate
@ -211,11 +361,12 @@ class MAPS(obja.Model):
Returns: Returns:
list[np.ndarray]: list the cartesian coordinates of the flattened 1-ring projected in the plane list[np.ndarray]: list the cartesian coordinates of the flattened 1-ring projected in the plane
""" """
ring, _ = self.one_ring(index) ring = self.vertices[index].vertex_ring
radius, angles = [], [] radius, angles = [], []
teta = 0.0 # cumulated angles teta = 0.0 # cumulated angles
for index1, index2 in sliding_window(ring): for index1, index2 in sliding_window(ring):
r = np.linalg.norm(self.vertices[index] - self.vertices[index1]) r = np.linalg.norm(
self.vertices[index].pos - self.vertices[index1].pos)
teta += self.compute_angle(index1, index, index2) # add new angle teta += self.compute_angle(index1, index, index2) # add new angle
radius.append(r) radius.append(r)
angles.append(teta) angles.append(teta)
@ -236,9 +387,9 @@ class MAPS(obja.Model):
Returns: Returns:
float: angle defined by the three points float: angle defined by the three points
""" """
a = self.vertices[i] a = self.vertices[i].pos
b = self.vertices[j] b = self.vertices[j].pos
c = self.vertices[k] c = self.vertices[k].pos
u = a - b u = a - b
v = c - b v = c - b
u /= np.linalg.norm(u) u /= np.linalg.norm(u)
@ -256,14 +407,55 @@ class MAPS(obja.Model):
Returns: Returns:
tuple[list[obja.Face], int]: list the triangles tuple[list[obja.Face], int]: list the triangles
""" """
polygon, ring = self.project_polar(index) polygon_, ring_ = self.project_polar(index)
main_v = []
for i, r in enumerate(ring_):
for feature_edge in self.feature_edges:
feat_edge_vertices = (feature_edge.a, feature_edge.b)
if r in feat_edge_vertices and index in feat_edge_vertices:
main_v.append(i)
if len(main_v) < 2:
polygons_rings = [(polygon_, ring_)]
else:
v1 = ring_[main_v[0]]
v2 = ring_[main_v[1]]
ring1, ring2 = [], []
polygon1, polygon2, = [], []
start = ring_.index(v1)
while ring_[start] != v2:
ring1.append(ring_[start])
polygon1.append(polygon_[start])
start += 1
start %= len(ring_)
ring1.append(ring_[start])
polygon1.append(polygon_[start])
start = ring_.index(v2)
while ring_[start] != v1:
ring2.append(ring_[start])
polygon2.append(polygon_[start])
start += 1
start %= len(ring_)
ring2.append(ring_[start])
polygon2.append(polygon_[start])
polygons_rings = [(polygon1, ring1), (polygon2, ring2)]
faces = [] # the final list of faces faces = [] # the final list of faces
for polygon, ring in polygons_rings:
indices = [(local_i, global_i) indices = [(local_i, global_i)
for local_i, global_i in enumerate(ring)] # remainging vertices for local_i, global_i in enumerate(ring)] # remainging vertices
node_index = 0 node_index = 0
cycle_counter = 0
while len(indices) > 2: while len(indices) > 2:
# Extract indices # Extract indices
local_i, global_i = indices[node_index - 1] local_i, global_i = indices[node_index - 1]
@ -277,7 +469,7 @@ class MAPS(obja.Model):
is_convex = MAPS.is_convex(prev_vert, curr_vert, next_vert) is_convex = MAPS.is_convex(prev_vert, curr_vert, next_vert)
is_ear = True is_ear = True
if is_convex: # the triangle needs to be convext to be an ear if is_convex or cycle_counter > len(indices): # the triangle needs to be convext to be an ear
# Begin with the point next to the triangle # Begin with the point next to the triangle
test_node_index = (node_index + 2) % len(indices) test_node_index = (node_index + 2) % len(indices)
while indices[test_node_index][0] != local_i and is_ear: while indices[test_node_index][0] != local_i and is_ear:
@ -289,10 +481,12 @@ class MAPS(obja.Model):
test_node_index = (test_node_index + 1) % len(indices) test_node_index = (test_node_index + 1) % len(indices)
else: else:
is_ear = False is_ear = False
cycle_counter += 1
if is_ear: if is_ear:
faces.append(obja.Face(global_i, global_j, global_k)) faces.append(Face(global_i, global_j, global_k))
indices.pop(node_index) # remove the point from the ring indices.pop(node_index) # remove the point from the ring
cycle_counter = 0
node_index = (node_index + 2) % len(indices) - 1 node_index = (node_index + 2) % len(indices) - 1
@ -355,24 +549,34 @@ class MAPS(obja.Model):
return (u >= 0) and (v >= 0) and (u + v < 1) return (u >= 0) and (v >= 0) and (u + v < 1)
def truc(self, output): def truc(self, output):
self.update()
priorities = self.compute_priority() priorities = self.compute_priority()
min_p = min(priorities) # min_p = min(priorities)
priorities = [p - min_p for p in priorities] # priorities = [p - min_p for p in priorities]
max_p = max(priorities) # max_p = max(priorities)
# colors = [priorities[face.a] + priorities[face.b] + priorities[face.c] for face in self.faces] colors = [priorities[face.a] + priorities[face.b] +
# min_c = min(colors) priorities[face.c] if face is not None else 0.0 for face in self.faces]
# colors = [c - min_c for c in colors] min_c = min(colors)
# max_c = max(colors) colors = [c - min_c for c in colors]
max_c = max(colors)
operations = [] operations = []
for i, face in enumerate(self.faces): for i, face in enumerate(self.faces):
if face != None: if face != None:
# r, g, b = colors[i] / max_c, 1.0, 1.0 r, g, b = colors[i] / max_c, 1.0, 1.0
# operations.append(('fc', i, (r, g, b))) c = 0
for x in (face.a, face.b, face.c):
for feature_edge in self.feature_edges:
if x in feature_edge:
c += 1
break
if c > 1:
r, g, b = 1.0, 0.0, 0.0
operations.append(('fc', i, (r, g, b), 0, 0, 0))
operations.append(('af', i, face, 0, 0, 0)) operations.append(('af', i, face, 0, 0, 0))
for i, vertex in enumerate(self.vertices): for i, vertex in enumerate(self.vertices):
if type(vertex) != NoneType: if vertex is None:
r, g, b = priorities[i] / max_p , 1.0, 1.0 # r, g, b = priorities[i] / max_p , 1.0, 1.0
operations.append(('av', i, vertex, r, g, b)) operations.append(('av', i, vertex, 1.0, 1.0, 1.0))
operations.reverse() operations.reverse()
# Write the result in output file # Write the result in output file
@ -396,8 +600,7 @@ class MAPS(obja.Model):
file=output file=output
) )
def compress(self, output: io.TextIOWrapper, final_only: bool) -> None:
def contract(self, output: io.TextIOWrapper) -> None:
""" Compress the 3d model """ Compress the 3d model
Args: Args:
@ -406,41 +609,45 @@ class MAPS(obja.Model):
operations = [] operations = []
# while len(self.vertices) > 64: # while len(self.vertices) > 64:
for i in range(1): for _ in range(2):
self.update()
selected_vertices = self.select_vertices() # find the set of vertices to remove selected_vertices = self.select_vertices() # find the set of vertices to remove
for vertex in track(selected_vertices, description="compression"): for v_index in track(selected_vertices, description="Compression"):
# print(f" {len(selected_vertices)} ", end='\r')
# Extract ring faces # Extract ring faces
_, ring_faces = self.one_ring(vertex) ring_faces = self.vertices[v_index].face_ring
# Apply retriangulation algorithm # Apply retriangulation algorithm
faces = self.clip_ear(vertex) faces = self.clip_ear(v_index)
# Edit the first faces # Edit the first faces
for i in range(len(faces)): for i in range(len(faces)):
# operations.append( if not final_only:
# ('ef', ring_faces[i], self.faces[ring_faces[i]])) operations.append(
('ef', ring_faces[i], self.faces[ring_faces[i]].to_obja()))
self.faces[ring_faces[i]] = faces[i] self.faces[ring_faces[i]] = faces[i]
# Remove the last faces # Remove the last faces
for i in range(len(faces), len(ring_faces)): for i in range(len(faces), len(ring_faces)):
# operations.append( if not final_only:
# ('af', ring_faces[i], self.faces[ring_faces[i]])) operations.append(
('af', ring_faces[i], self.faces[ring_faces[i]].to_obja()))
self.faces[ring_faces[i]] = None self.faces[ring_faces[i]] = None
# Remove the vertex # Remove the vertex
# operations.append(('av', vertex, self.vertices[vertex])) if not final_only:
self.vertices[vertex] = None operations.append(
('av', v_index, self.vertices[v_index].to_obja()))
self.vertices[v_index] = None
# Register remaining vertices and faces # Register remaining vertices and faces
for i, face in enumerate(self.faces): for i, face in enumerate(self.faces):
if face != None: if face is not None:
operations.append(('af', i, face)) operations.append(('af', i, face.to_obja()))
for i, vertex in enumerate(self.vertices): for i, v_index in enumerate(self.vertices):
if type(vertex) != NoneType: if v_index is not None:
operations.append(('av', i, vertex)) operations.append(('av', i, v_index.to_obja()))
# To rebuild the model, run operations in reverse order # To rebuild the model, run operations in reverse order
operations.reverse() operations.reverse()
@ -477,7 +684,9 @@ def main(args):
model.parse_file(args.input) model.parse_file(args.input)
with open(args.output, 'w') as output: with open(args.output, 'w') as output:
model.truc(output) model.compress(output, args.final)
# with open(args.output, 'w') as output:
# model.truc(output)
if __name__ == '__main__': if __name__ == '__main__':
@ -485,6 +694,7 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', type=str, required=True) parser.add_argument('-i', '--input', type=str, required=True)
parser.add_argument('-o', '--output', type=str, required=True) parser.add_argument('-o', '--output', type=str, required=True)
parser.add_argument('-f', '--final', type=bool, default=False)
args = parser.parse_args() args = parser.parse_args()
main(args) main(args)