From 7318207dbd4a1f49b1bf1ce86746a049d2a03dfb Mon Sep 17 00:00:00 2001 From: gdamms Date: Wed, 19 Oct 2022 15:46:29 +0200 Subject: [PATCH] feat: OOOOOOOOOOOOOOOOOOO --- main.py | 377 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 218 insertions(+), 159 deletions(-) diff --git a/main.py b/main.py index 6445def..5f1674b 100644 --- a/main.py +++ b/main.py @@ -7,32 +7,63 @@ import argparse from rich.progress import Progress -def cot(x: float): +def cot(x: float) -> float: + """Cotangeante of x + + Args: + x (float): angle + + Returns: + float: cotangeante of x + """ sin_x = np.sin(x) if sin_x == 0: return 1e16 return np.cos(x) / sin_x -def sliding_window(l: list, n: int = 2): +def sliding_window(l: list, n: int = 2) -> list[tuple]: + """Create a sliding window of size n + + Args: + l (list): list to create the sliding window from + n (int, optional): number of value. Defaults to 2. + + Returns: + list[tuple]: sliding window + """ k = n - 1 l2 = l + [l[i] for i in range(k)] res = [(x for x in l2[i:i+n]) for i in range(len(l2)-k)] return res - class Edge: - def __init__(self, a, b): + """Edge representation""" + def __init__(self, a: int, b: int) -> None: + """Create an edge + + Args: + a (int): first vertex + b (int): second vertex + """ self.a = min(a, b) self.b = max(a, b) - self.face1 = None - self.face2 = None + self.face1: Face | None = None + self.face2: Face | None = None - self.fold = 0.0 - self.curvature = 0.0 + self.fold: float = 0.0 + self.curvature: float = 0.0 def __eq__(self, __o: object) -> bool: + """Check if two edges are equal + + Args: + __o (object): other edge + + Returns: + bool: True if equal, False otherwise + """ if isinstance(__o, Edge): return self.a == __o.a and self.b == __o.b @@ -40,17 +71,43 @@ class Edge: class Face: - def __init__(self, a, b, c): + """Face representation""" + def __init__(self, a: int, b: int, c: int) -> None: + """Face constructor + + Args: + a (int): first vertex index + b (int): second vertex index + c (int): third vertex index + """ + self.a = a + self.b = b + self.c = c + + self.edges: list[Edge] = [] self.a = a self.b = b self.c = c self.normal = np.zeros(3) - def to_obja(self): + def to_obja(self) -> obja.Face: + """Convert face to obja format + + Returns: + obja.Face: face in obja format + """ return obja.Face(self.a, self.b, self.c) def __eq__(self, __o: object) -> bool: + """Check if two faces are equal + + Args: + __o (object): other face + + Returns: + bool: True if equal, False otherwise + """ if isinstance(__o, Face): return set((__o.a, __o.b, __o.c)) == set((self.a, self.b, self.c)) @@ -58,138 +115,161 @@ class Face: class Vertex: - def __init__(self, pos): + """Vertex representation""" + def __init__(self, pos: np.ndarray[int, float]) -> None: + """Vertex constructor + + Args: + pos (np.ndarray[int, float]): position of the vertex + """ self.pos = pos - self.vertex_ring = [] - self.face_ring = [] + self.vertex_ring: list[int] = [] + self.face_ring: list[int] = [] - self.normal = np.zeros(3) + self.normal: np.ndarray = np.zeros(3) - self.area = 0.0 - self.curvature = 0.0 + self.area: float = 0.0 + self.curvature: float = 0.0 - def to_obja(self): + def to_obja(self) -> np.ndarray: + """Convert vertex to obja format + + Returns: + np.ndarray: vertex in obja format + """ return self.pos class MAPS(obja.Model): - """_summary_ - - Args: - obja (_type_): _description_ - """ + """MAPS compression model""" def __init__(self): + """MAPS constructor""" super().__init__() - def parse_file(self, path): + def parse_file(self, path: str) -> None: + """Parse a file + + Args: + path (str): path to the file + """ 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): + def update(self) -> None: + """Precompute things""" + # Reset progress bars self.progress.reset(self.select_task, total=len(self.vertices)) self.progress.reset(self.compress_task) + + # Compute usefull things self.update_rings() self.update_edges() self.update_normals() self.update_area_curvature() - def fix(self): + def fix(self) -> None: + """Fix the model""" + fixed = True + + # Find a vertex with less than 3 faces for i, vertex in enumerate(self.vertices): if vertex is None: continue - if len(vertex.face_ring) < 3: - for face in vertex.face_ring: - self.faces[face] = None - self.vertices[i] = None - fixed = False - return fixed - - def update_edges(self): + if len(vertex.face_ring) < 3: + # Remove the vertex and its faces + for face in vertex.face_ring: + if not self.final_only: + self.operations.append( + ('af', face, self.faces[face].to_obja())) + self.faces[face] = None + if not self.final_only: + self.operations.append( + ('av', i, vertex.to_obja())) + self.vertices[i] = None + + # Indicate that the model has to be fixed again + fixed = False + + return fixed + + def update_edges(self) -> None: + """Update edges""" + self.edges = {} for face in self.faces: if face is None: continue + # Create all edges for the face for a, b in sliding_window([face.a, face.b, face.c], n=2): new_edge = Edge(a, b) - if f"{new_edge.a}:{new_edge.b}" not in self.edges.keys(): - new_edge.face1 = face - for face2_i in self.vertices[new_edge.a].face_ring: - face2 = self.faces[face2_i] - if face2 == face: - continue + # Check if the edge already exists + if f"{new_edge.a}:{new_edge.b}" in self.edges.keys(): + continue - face2_vertices = (face2.a, face2.b, face2.c) - if not (a in face2_vertices and b in face2_vertices): - continue + new_edge.face1 = face - new_edge.face2 = face2 - break - - self.edges[f"{new_edge.a}:{new_edge.b}"] = new_edge - - def update_rings(self): - try: - fixed = False - while not fixed: - for vertex in self.vertices: - if vertex is None: + # Find the opoosite face + for face2_i in self.vertices[new_edge.a].face_ring: + face2 = self.faces[face2_i] + if face2 == face: continue - vertex.face_ring = [] - for i, face in enumerate(self.faces): - if face is None: + face2_vertices = (face2.a, face2.b, face2.c) + if not (a in face2_vertices and b in face2_vertices): continue - for vertex_i in (face.a, face.b, face.c): - self.vertices[vertex_i].face_ring.append(i) - - fixed = self.fix() - for i, vertex in enumerate(self.vertices): - vertex = self.vertices[i] + new_edge.face2 = face2 + break + + # Add the new edge to the list + self.edges[f"{new_edge.a}:{new_edge.b}"] = new_edge + + def update_rings(self) -> None: + """Update vertex and face rings""" + + fixed = False + + # Wait till the model is fixed + while not fixed: + for vertex in self.vertices: + # Reset vertex ring if vertex is None: continue - if len(vertex.face_ring) == 0: - self.vertices[i] = None - continue - ring = self.one_ring(i) - vertex.vertex_ring = ring - except ValueError: - self.update_rings() - + vertex.face_ring = [] - def fail(self, index): - print('fail') - output_file = open('obja/example/fail.obja', 'w') - output = obja.Output(output_file) - used = [] - for i, x in enumerate(self.vertices[index].face_ring): - face = self.faces[x] - for y in (face.a, face.b, face.c): - if y in used: + # Add faces to vertex ring + for i, face in enumerate(self.faces): + if face is None: continue - output.add_vertex(y, self.vertices[y].to_obja()) - used.append(y) - output.add_face(x, face.to_obja()) - print('fc {} {} {} {}'.format( - i + 1, - np.random.rand(), - np.random.rand(), - np.random.rand()), - file=output_file - ) - print(x, (face.a, face.b, face.c)) + for vertex_i in (face.a, face.b, face.c): + self.vertices[vertex_i].face_ring.append(i) - def update_area_curvature(self): + # Fix the model + fixed = self.fix() + + for i, vertex in enumerate(self.vertices): + vertex = self.vertices[i] + if vertex is None: + continue + + # Compute rings + ring = self.one_ring(i) + vertex.vertex_ring = ring + + def update_area_curvature(self) -> None: + """Update area and curvature""" + + # Get the area and curvature of each vertex for i, vertex in enumerate(self.vertices): if vertex is None: continue @@ -198,34 +278,39 @@ class MAPS(obja.Model): vertex.area = area vertex.curvature = curvature + # Find feature edges self.feature_edges = [] for edge in self.edges.values(): - if edge.face2 is None: - self.fail(edge.b) edge.fold = np.dot(edge.face1.normal, edge.face2.normal) if edge.fold < 0.5: self.feature_edges.append(edge) - def update_normals(self): + def update_normals(self) -> None: + """Update normals""" for face in self.faces: if face is None: continue + # Compute face normal 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) + norm = np.linalg.norm(n) + if norm != 0: + n /= np.linalg.norm(n) face.normal = n + # Sum vertex normal self.vertices[face.a].normal += n self.vertices[face.b].normal += n self.vertices[face.c].normal += n + # Normalize vertex normals for vertex in self.vertices: if vertex is None: continue @@ -235,7 +320,7 @@ class MAPS(obja.Model): vertex.normal /= norm def one_ring(self, index: int) -> list[int]: - """ Return the corresponding 1-ring + """Return the corresponding 1-ring Args: index (int): index of the 1-ring's main vertex @@ -271,40 +356,12 @@ class MAPS(obja.Model): break if not broke: - self.fail(index) - - for i, face_i in enumerate(self.vertices[index].face_ring): - for face_j in self.vertices[index].face_ring[i+1:]: - face1 = self.faces[face_i] - face2 = self.faces[face_j] - if face1 == face2: - self.faces[face_i] = None - self.faces[face_j] = None - verts = (face1.a, face1.b, face1.c) - for vert in verts: - if vert == index: - continue - to_remove = True - for face_k in self.vertices[vert].face_ring: - face = self.faces[face_k] - if face is None: - continue - if vert in (face.a, face.b, face.c): - to_remove = False - break - if to_remove: - self.vertices[vert] = None - break - break - - - raise ValueError( - f"Vertex {prev_index} is not in the remaining faces {ring_faces}. Origin {ring} on {index}") + raise Exception('Ring not corrupted') return ring 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: index (int): index of the 1-ring's main vertex @@ -426,7 +483,7 @@ class MAPS(obja.Model): return selected_vertices def project_polar(self, index: int) -> tuple[list[np.ndarray], list[int]]: - """ Flatten the 1-ring to retriangulate + """Flatten the 1-ring to retriangulate Args: index (int): main vertex of the 1-ring @@ -450,7 +507,7 @@ class MAPS(obja.Model): return coordinates, ring def compute_angle(self, i: int, j: int, k: int) -> float: - """ Calculate the angle defined by three points + """Calculate the angle defined by three points Args: i (int): previous index @@ -472,7 +529,7 @@ class MAPS(obja.Model): return np.arccos(np.clip(res, -1, 1)) def clip_ear(self, index: int) -> list[obja.Face]: - """ Retriangulate a polygon using the ear clipping algorithm + """Retriangulate a polygon using the ear clipping algorithm Args: index (int): index of 1-ring @@ -567,11 +624,11 @@ class MAPS(obja.Model): return faces def is_convex(self, - prev_vert: np.ndarray[int, np.dtype[np.float64]], - curr_vert: np.ndarray[int, np.dtype[np.float64]], - next_vert: np.ndarray[int, np.dtype[np.float64]] + prev_vert: np.ndarray[int, float], + curr_vert: np.ndarray[int, float], + next_vert: np.ndarray[int, float], ) -> bool: - """ Check if the angle less than pi + """Check if the angle less than pi Args: prev_vert (np.ndarray): first point @@ -592,12 +649,12 @@ class MAPS(obja.Model): return internal_angle >= np.pi def is_inside(self, - a: np.ndarray[int, np.dtype[np.float64]], - b: np.ndarray[int, np.dtype[np.float64]], - c: np.ndarray[int, np.dtype[np.float64]], - p: np.ndarray[int, np.dtype[np.float64]] + a: np.ndarray[int, float], + b: np.ndarray[int, float], + c: np.ndarray[int, float], + p: np.ndarray[int, float], ) -> bool: - """ Check if p is in the triangle a b c + """Check if p is in the triangle a b c Args: a (np.ndarray): point one @@ -642,7 +699,7 @@ class MAPS(obja.Model): colors = [c - min_c for c in colors] max_c = max(colors) - operations = [] + self.operations = [] for i, face in enumerate(self.faces): if face is None: continue @@ -654,19 +711,19 @@ class MAPS(obja.Model): r, g, b = 1.0, 0.0, 0.0 break - operations.append(('fc', i, (r, g, b))) - operations.append(('af', i, face.to_obja())) + self.operations.append(('fc', i, (r, g, b))) + self.operations.append(('af', i, face.to_obja())) for i, vertex in enumerate(self.vertices): if vertex is None: continue - operations.append(('av', i, vertex.to_obja())) - operations.reverse() + self.operations.append(('av', i, vertex.to_obja())) + self.operations.reverse() # Write the result in output file output_model = obja.Output(output) - for (op, index, value) in operations: + for (op, index, value) in self.operations: if op == 'av': output_model.add_vertex(index, value) elif op == 'af': @@ -685,7 +742,7 @@ class MAPS(obja.Model): ) def compress(self, output: io.TextIOWrapper, level: int, final_only: bool, debug: bool) -> None: - """ Compress the 3d model + """Compress the 3d model Args: output (io.TextIOWrapper): Output file descriptor @@ -697,7 +754,8 @@ class MAPS(obja.Model): self.compress_task = progress.add_task('╙── Compression') self.progress = progress - operations = [] + self.operations = [] + self.final_only = final_only # while len(self.vertices) > 64: for _ in progress.track(range(level), task_id=self.global_task): @@ -714,43 +772,43 @@ class MAPS(obja.Model): # Edit the first faces for i in range(len(faces)): - if not final_only: - operations.append( + if not self.final_only: + self.operations.append( ('ef', ring_faces[i], self.faces[ring_faces[i]].to_obja())) self.faces[ring_faces[i]] = faces[i] # Remove the last faces for i in range(len(faces), len(ring_faces)): - if not final_only: - operations.append( + if not self.final_only: + self.operations.append( ('af', ring_faces[i], self.faces[ring_faces[i]].to_obja())) self.faces[ring_faces[i]] = None # Remove the vertex - if not final_only: - operations.append( + if not self.final_only: + self.operations.append( ('av', v_index, self.vertices[v_index].to_obja())) self.vertices[v_index] = None if debug: self.debug(output) return - + # Register remaining vertices and faces for i, face in enumerate(self.faces): if face is not None: - operations.append(('af', i, face.to_obja())) + self.operations.append(('af', i, face.to_obja())) for i, v_index in enumerate(self.vertices): if v_index is not None: - operations.append(('av', i, v_index.to_obja())) + self.operations.append(('av', i, v_index.to_obja())) # To rebuild the model, run operations in reverse order - operations.reverse() + self.operations.reverse() # Write the result in output file output_model = obja.Output(output) - for (op, index, value) in operations: + for (op, index, value) in self.operations: if op == 'av': output_model.add_vertex(index, value) elif op == 'af': @@ -773,7 +831,7 @@ class MAPS(obja.Model): def main(args): - """ Run MAPS model compression + """Run MAPS model compression Args: args (Namespace): arguments (input and output path) @@ -782,7 +840,8 @@ def main(args): model.parse_file(args.input) with open(args.output, 'w') as output: - model.compress(output, args.level, args.final or args.debug, args.debug) + model.compress(output, args.level, + args.final or args.debug, args.debug) if __name__ == '__main__':