feat: OOOOOOOOOOOOOOOOOOO

This commit is contained in:
gdamms 2022-10-19 15:46:29 +02:00
parent 1b40190198
commit 7318207dbd

287
main.py
View file

@ -7,32 +7,63 @@ import argparse
from rich.progress import Progress 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) sin_x = np.sin(x)
if sin_x == 0: if sin_x == 0:
return 1e16 return 1e16
return np.cos(x) / sin_x 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 k = n - 1
l2 = l + [l[i] for i in range(k)] 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)] res = [(x for x in l2[i:i+n]) for i in range(len(l2)-k)]
return res return res
class Edge: 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.a = min(a, b)
self.b = max(a, b) self.b = max(a, b)
self.face1 = None self.face1: Face | None = None
self.face2 = None self.face2: Face | None = None
self.fold = 0.0 self.fold: float = 0.0
self.curvature = 0.0 self.curvature: float = 0.0
def __eq__(self, __o: object) -> bool: 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): if isinstance(__o, Edge):
return self.a == __o.a and self.b == __o.b return self.a == __o.a and self.b == __o.b
@ -40,17 +71,43 @@ class Edge:
class Face: 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.a = a
self.b = b self.b = b
self.c = c self.c = c
self.normal = np.zeros(3) 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) return obja.Face(self.a, self.b, self.c)
def __eq__(self, __o: object) -> bool: 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): if isinstance(__o, Face):
return set((__o.a, __o.b, __o.c)) == set((self.a, self.b, self.c)) return set((__o.a, __o.b, __o.c)) == set((self.a, self.b, self.c))
@ -58,71 +115,110 @@ class Face:
class Vertex: 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.pos = pos
self.vertex_ring = [] self.vertex_ring: list[int] = []
self.face_ring = [] self.face_ring: list[int] = []
self.normal = np.zeros(3) self.normal: np.ndarray = np.zeros(3)
self.area = 0.0 self.area: float = 0.0
self.curvature = 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 return self.pos
class MAPS(obja.Model): class MAPS(obja.Model):
"""_summary_ """MAPS compression model"""
Args:
obja (_type_): _description_
"""
def __init__(self): def __init__(self):
"""MAPS constructor"""
super().__init__() 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) super().parse_file(path)
for i, vertex in enumerate(self.vertices): for i, vertex in enumerate(self.vertices):
self.vertices[i] = Vertex(vertex) self.vertices[i] = Vertex(vertex)
for i, face in enumerate(self.faces): for i, face in enumerate(self.faces):
self.faces[i] = Face(face.a, face.b, face.c) 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.select_task, total=len(self.vertices))
self.progress.reset(self.compress_task) self.progress.reset(self.compress_task)
# Compute usefull things
self.update_rings() self.update_rings()
self.update_edges() self.update_edges()
self.update_normals() self.update_normals()
self.update_area_curvature() self.update_area_curvature()
def fix(self): def fix(self) -> None:
"""Fix the model"""
fixed = True fixed = True
# Find a vertex with less than 3 faces
for i, vertex in enumerate(self.vertices): for i, vertex in enumerate(self.vertices):
if vertex is None: if vertex is None:
continue continue
if len(vertex.face_ring) < 3: if len(vertex.face_ring) < 3:
# Remove the vertex and its faces
for face in vertex.face_ring: 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 self.faces[face] = None
if not self.final_only:
self.operations.append(
('av', i, vertex.to_obja()))
self.vertices[i] = None self.vertices[i] = None
# Indicate that the model has to be fixed again
fixed = False fixed = False
return fixed return fixed
def update_edges(self) -> None:
"""Update edges"""
def update_edges(self):
self.edges = {} self.edges = {}
for face in self.faces: for face in self.faces:
if face is None: if face is None:
continue continue
# Create all edges for the face
for a, b in sliding_window([face.a, face.b, face.c], n=2): for a, b in sliding_window([face.a, face.b, face.c], n=2):
new_edge = Edge(a, b) new_edge = Edge(a, b)
if f"{new_edge.a}:{new_edge.b}" not in self.edges.keys():
# Check if the edge already exists
if f"{new_edge.a}:{new_edge.b}" in self.edges.keys():
continue
new_edge.face1 = face new_edge.face1 = face
# Find the opoosite face
for face2_i in self.vertices[new_edge.a].face_ring: for face2_i in self.vertices[new_edge.a].face_ring:
face2 = self.faces[face2_i] face2 = self.faces[face2_i]
if face2 == face: if face2 == face:
@ -135,61 +231,45 @@ class MAPS(obja.Model):
new_edge.face2 = face2 new_edge.face2 = face2
break break
# Add the new edge to the list
self.edges[f"{new_edge.a}:{new_edge.b}"] = new_edge self.edges[f"{new_edge.a}:{new_edge.b}"] = new_edge
def update_rings(self): def update_rings(self) -> None:
try: """Update vertex and face rings"""
fixed = False fixed = False
# Wait till the model is fixed
while not fixed: while not fixed:
for vertex in self.vertices: for vertex in self.vertices:
# Reset vertex ring
if vertex is None: if vertex is None:
continue continue
vertex.face_ring = [] vertex.face_ring = []
# Add faces to vertex ring
for i, face in enumerate(self.faces): for i, face in enumerate(self.faces):
if face is None: if face is None:
continue continue
for vertex_i in (face.a, face.b, face.c): for vertex_i in (face.a, face.b, face.c):
self.vertices[vertex_i].face_ring.append(i) self.vertices[vertex_i].face_ring.append(i)
# Fix the model
fixed = self.fix() fixed = self.fix()
for i, vertex in enumerate(self.vertices): for i, vertex in enumerate(self.vertices):
vertex = self.vertices[i] vertex = self.vertices[i]
if vertex is None: if vertex is None:
continue continue
if len(vertex.face_ring) == 0:
self.vertices[i] = None # Compute rings
continue
ring = self.one_ring(i) ring = self.one_ring(i)
vertex.vertex_ring = ring vertex.vertex_ring = ring
except ValueError:
self.update_rings()
def update_area_curvature(self) -> None:
"""Update area and curvature"""
def fail(self, index): # Get the area and curvature of each vertex
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:
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))
def update_area_curvature(self):
for i, vertex in enumerate(self.vertices): for i, vertex in enumerate(self.vertices):
if vertex is None: if vertex is None:
continue continue
@ -198,34 +278,39 @@ class MAPS(obja.Model):
vertex.area = area vertex.area = area
vertex.curvature = curvature vertex.curvature = curvature
# Find feature edges
self.feature_edges = [] self.feature_edges = []
for edge in self.edges.values(): 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) edge.fold = np.dot(edge.face1.normal, edge.face2.normal)
if edge.fold < 0.5: if edge.fold < 0.5:
self.feature_edges.append(edge) self.feature_edges.append(edge)
def update_normals(self): def update_normals(self) -> None:
"""Update normals"""
for face in self.faces: for face in self.faces:
if face is None: if face is None:
continue continue
# Compute face normal
p1 = self.vertices[face.a].pos p1 = self.vertices[face.a].pos
p2 = self.vertices[face.b].pos p2 = self.vertices[face.b].pos
p3 = self.vertices[face.c].pos p3 = self.vertices[face.c].pos
u = p2 - p1 u = p2 - p1
v = p3 - p1 v = p3 - p1
n = np.cross(u, v) n = np.cross(u, v)
norm = np.linalg.norm(n)
if norm != 0:
n /= np.linalg.norm(n) n /= np.linalg.norm(n)
face.normal = n face.normal = n
# Sum vertex normal
self.vertices[face.a].normal += n self.vertices[face.a].normal += n
self.vertices[face.b].normal += n self.vertices[face.b].normal += n
self.vertices[face.c].normal += n self.vertices[face.c].normal += n
# Normalize vertex normals
for vertex in self.vertices: for vertex in self.vertices:
if vertex is None: if vertex is None:
continue continue
@ -271,35 +356,7 @@ class MAPS(obja.Model):
break break
if not broke: if not broke:
self.fail(index) raise Exception('Ring not corrupted')
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}")
return ring return ring
@ -567,9 +624,9 @@ class MAPS(obja.Model):
return faces return faces
def is_convex(self, def is_convex(self,
prev_vert: np.ndarray[int, np.dtype[np.float64]], prev_vert: np.ndarray[int, float],
curr_vert: np.ndarray[int, np.dtype[np.float64]], curr_vert: np.ndarray[int, float],
next_vert: np.ndarray[int, np.dtype[np.float64]] next_vert: np.ndarray[int, float],
) -> bool: ) -> bool:
"""Check if the angle less than pi """Check if the angle less than pi
@ -592,10 +649,10 @@ class MAPS(obja.Model):
return internal_angle >= np.pi return internal_angle >= np.pi
def is_inside(self, def is_inside(self,
a: np.ndarray[int, np.dtype[np.float64]], a: np.ndarray[int, float],
b: np.ndarray[int, np.dtype[np.float64]], b: np.ndarray[int, float],
c: np.ndarray[int, np.dtype[np.float64]], c: np.ndarray[int, float],
p: np.ndarray[int, np.dtype[np.float64]] p: np.ndarray[int, float],
) -> bool: ) -> bool:
"""Check if p is in the triangle a b c """Check if p is in the triangle a b c
@ -642,7 +699,7 @@ class MAPS(obja.Model):
colors = [c - min_c for c in colors] colors = [c - min_c for c in colors]
max_c = max(colors) max_c = max(colors)
operations = [] self.operations = []
for i, face in enumerate(self.faces): for i, face in enumerate(self.faces):
if face is None: if face is None:
continue continue
@ -654,19 +711,19 @@ class MAPS(obja.Model):
r, g, b = 1.0, 0.0, 0.0 r, g, b = 1.0, 0.0, 0.0
break break
operations.append(('fc', i, (r, g, b))) self.operations.append(('fc', i, (r, g, b)))
operations.append(('af', i, face.to_obja())) self.operations.append(('af', i, face.to_obja()))
for i, vertex in enumerate(self.vertices): for i, vertex in enumerate(self.vertices):
if vertex is None: if vertex is None:
continue continue
operations.append(('av', i, vertex.to_obja())) self.operations.append(('av', i, vertex.to_obja()))
operations.reverse() self.operations.reverse()
# Write the result in output file # Write the result in output file
output_model = obja.Output(output) output_model = obja.Output(output)
for (op, index, value) in operations: for (op, index, value) in self.operations:
if op == 'av': if op == 'av':
output_model.add_vertex(index, value) output_model.add_vertex(index, value)
elif op == 'af': elif op == 'af':
@ -697,7 +754,8 @@ class MAPS(obja.Model):
self.compress_task = progress.add_task('╙── Compression') self.compress_task = progress.add_task('╙── Compression')
self.progress = progress self.progress = progress
operations = [] self.operations = []
self.final_only = final_only
# while len(self.vertices) > 64: # while len(self.vertices) > 64:
for _ in progress.track(range(level), task_id=self.global_task): for _ in progress.track(range(level), task_id=self.global_task):
@ -714,21 +772,21 @@ class MAPS(obja.Model):
# Edit the first faces # Edit the first faces
for i in range(len(faces)): for i in range(len(faces)):
if not final_only: if not self.final_only:
operations.append( self.operations.append(
('ef', ring_faces[i], self.faces[ring_faces[i]].to_obja())) ('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)):
if not final_only: if not self.final_only:
operations.append( self.operations.append(
('af', ring_faces[i], self.faces[ring_faces[i]].to_obja())) ('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
if not final_only: if not self.final_only:
operations.append( self.operations.append(
('av', v_index, self.vertices[v_index].to_obja())) ('av', v_index, self.vertices[v_index].to_obja()))
self.vertices[v_index] = None self.vertices[v_index] = None
@ -739,18 +797,18 @@ class MAPS(obja.Model):
# 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 is not None: 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): for i, v_index in enumerate(self.vertices):
if v_index is not None: 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 # To rebuild the model, run operations in reverse order
operations.reverse() self.operations.reverse()
# Write the result in output file # Write the result in output file
output_model = obja.Output(output) output_model = obja.Output(output)
for (op, index, value) in operations: for (op, index, value) in self.operations:
if op == 'av': if op == 'av':
output_model.add_vertex(index, value) output_model.add_vertex(index, value)
elif op == 'af': elif op == 'af':
@ -782,7 +840,8 @@ 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.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__': if __name__ == '__main__':