107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
import numpy as np
|
|
from stl import mesh
|
|
import struct
|
|
import sys
|
|
import os
|
|
|
|
def simplify_stl(input_path, output_path, keep_ratio=0.1):
|
|
try:
|
|
try:
|
|
import trimesh
|
|
mesh_data = trimesh.load(input_path, file_type='stl')
|
|
if hasattr(mesh_data, 'triangles'):
|
|
vertices = mesh_data.triangles.reshape(-1, 3)
|
|
else:
|
|
vertices = mesh_data.vertices[mesh_data.faces].reshape(-1, 3)
|
|
use_trimesh = True
|
|
except ImportError:
|
|
# Load mesh using numpy-stl fallback
|
|
m = mesh.Mesh.from_file(input_path)
|
|
vertices = m.vectors.reshape(-1, 3)
|
|
use_trimesh = False
|
|
|
|
min_v = vertices.min(axis=0)
|
|
max_v = vertices.max(axis=0)
|
|
bbox_size = max_v - min_v
|
|
max_dim = np.max(bbox_size)
|
|
|
|
if max_dim == 0:
|
|
if use_trimesh:
|
|
mesh_data.export(output_path, file_type='stl')
|
|
else:
|
|
m.save(output_path)
|
|
return True
|
|
|
|
# Target roughly a resolution that gives us keep_ratio faces.
|
|
# This is a heuristic approach to grid-based vertex clustering.
|
|
# Function to simplify given a grid size
|
|
def do_simplify(g_size):
|
|
v_idx = np.round((vertices - min_v) / g_size).astype(np.int64)
|
|
|
|
# Fast 1D hash to avoid extremely slow np.unique(axis=0) on 2D arrays
|
|
max_idx = v_idx.max(axis=0) + 1
|
|
v_1d = v_idx[:, 0] + v_idx[:, 1] * max_idx[0] + v_idx[:, 2] * max_idx[0] * max_idx[1]
|
|
|
|
# Find unique grid cells and map old vertices to them
|
|
_, unique_idx, inv_idx = np.unique(v_1d, return_index=True, return_inverse=True)
|
|
new_verts = vertices[unique_idx]
|
|
|
|
# Map faces to new vertices
|
|
faces = inv_idx.reshape(-1, 3)
|
|
|
|
# Remove degenerate faces (faces where at least two vertices resolve to the same cell)
|
|
valid = (faces[:,0] != faces[:,1]) & (faces[:,1] != faces[:,2]) & (faces[:,0] != faces[:,2])
|
|
valid_faces = faces[valid]
|
|
|
|
return new_verts, valid_faces
|
|
|
|
target_faces = max(1, int((len(vertices) // 3) * keep_ratio))
|
|
low_g = max_dim * 0.0005
|
|
high_g = max_dim * 0.2
|
|
best_verts = vertices
|
|
best_faces = np.arange(len(vertices)).reshape(-1, 3)
|
|
|
|
# Binary search for the right grid size
|
|
for _ in range(8):
|
|
g_size = (low_g + high_g) / 2
|
|
v, f = do_simplify(g_size)
|
|
best_verts, best_faces = v, f
|
|
|
|
if len(f) > target_faces:
|
|
# too many faces, make grid coarser (larger)
|
|
low_g = g_size
|
|
else:
|
|
# too few faces, make grid finer (smaller)
|
|
high_g = g_size
|
|
|
|
if abs(len(f) - target_faces) < target_faces * 0.05:
|
|
break
|
|
|
|
new_vertices, valid_faces = best_verts, best_faces
|
|
|
|
if use_trimesh:
|
|
simplified = trimesh.Trimesh(vertices=new_vertices, faces=valid_faces, process=False)
|
|
simplified.export(output_path, file_type='stl')
|
|
return True
|
|
|
|
# Build the simplified mesh using fallback
|
|
new_m = mesh.Mesh(np.zeros(valid_faces.shape[0], dtype=mesh.Mesh.dtype))
|
|
|
|
# Vectorized assignment
|
|
new_m.vectors[:, 0, :] = new_vertices[valid_faces[:, 0]]
|
|
new_m.vectors[:, 1, :] = new_vertices[valid_faces[:, 1]]
|
|
new_m.vectors[:, 2, :] = new_vertices[valid_faces[:, 2]]
|
|
|
|
# Calculate normals correctly
|
|
new_m.update_normals()
|
|
new_m.save(output_path)
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Error simplifying STL: {e}")
|
|
return False
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) > 2:
|
|
simplify_stl(sys.argv[1], sys.argv[2])
|