feat(ml): auto-commit con 7 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""Decima (simplifica) una malla 3D GLB/OBJ/PLY conservando su apariencia.
|
||||
|
||||
Post-proceso de las mallas densas que produce el pipeline Hunyuan3D de ComfyUI
|
||||
(60 MB / ~1.67 M caras). Suelda los vertices duplicados ANTES del collapse —
|
||||
las mallas de VoxelToMeshBasic son "cube-soup" (4 vertices nuevos por cara, sin
|
||||
compartir aristas) y el quadric edge collapse no las decima si no se sueldan
|
||||
primero. Luego aplica meshing_decimation_quadric_edge_collapse (pymeshlab), el
|
||||
mismo filtro que el FaceReducer del wrapper Hy3D usa internamente.
|
||||
|
||||
Conserva la apariencia segun el tipo del original:
|
||||
- Vertex colors (ColorVisuals, lo habitual en el shape stage de Hunyuan3D):
|
||||
pymeshlab los interpola a traves del collapse y se vuelven a adjuntar.
|
||||
- Textura (TextureVisuals con UV + baseColorTexture): reproyecta las UV por
|
||||
vertice mas cercano (scipy cKDTree, sin rtree) y readjunta la imagen.
|
||||
- Sin apariencia: simplifica la geometria igual.
|
||||
|
||||
Impura: lee y escribe archivos en disco. Requiere trimesh + pymeshlab + scipy.
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _err(in_faces, in_mb, msg):
|
||||
return {
|
||||
"ok": False, "in_faces": in_faces, "out_faces": 0,
|
||||
"in_mb": in_mb, "out_mb": 0.0, "out_path": "",
|
||||
"welded_faces": 0, "decimate_status": "", "appearance": "",
|
||||
"error": msg,
|
||||
}
|
||||
|
||||
|
||||
def _load_mesh(path):
|
||||
import trimesh
|
||||
|
||||
obj = trimesh.load(path, process=False)
|
||||
if isinstance(obj, trimesh.Scene):
|
||||
obj = trimesh.util.concatenate(list(obj.geometry.values()))
|
||||
return obj
|
||||
|
||||
|
||||
def _detect_appearance(mesh):
|
||||
"""Devuelve (kind, attr, image): 'vertex_color'|'texture'|'none'."""
|
||||
import trimesh
|
||||
|
||||
vis = mesh.visual
|
||||
if isinstance(vis, trimesh.visual.texture.TextureVisuals) and vis.uv is not None:
|
||||
img = getattr(getattr(vis, "material", None), "baseColorTexture", None)
|
||||
if img is not None:
|
||||
return "texture", np.asarray(vis.uv, dtype=np.float64), img
|
||||
if isinstance(vis, trimesh.visual.color.ColorVisuals):
|
||||
vc = vis.vertex_colors
|
||||
if vc is not None and len(vc) == len(mesh.vertices):
|
||||
return "vertex_color", np.asarray(vc, dtype=np.uint8), None
|
||||
return "none", None, None
|
||||
|
||||
|
||||
def comfyui_simplify_mesh(
|
||||
in_path: str,
|
||||
*,
|
||||
target_faces: int = 80000,
|
||||
weld: bool = True,
|
||||
out_path: str | None = None,
|
||||
) -> dict:
|
||||
"""Decima una malla GLB/OBJ/PLY a target_faces conservando su apariencia.
|
||||
|
||||
Args:
|
||||
in_path: ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off).
|
||||
target_faces: numero de caras objetivo tras la decimacion. keyword-only.
|
||||
weld: si True (default), suelda vertices duplicados antes del collapse.
|
||||
Imprescindible para las mallas cube-soup de VoxelToMeshBasic; sin el,
|
||||
el collapse no reduce caras. keyword-only.
|
||||
out_path: ruta de salida. Si None, escribe "<in>_simplified.glb" junto al
|
||||
original. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, in_faces, out_faces, in_mb, out_mb, out_path, welded_faces,
|
||||
decimate_status, appearance, error}. Si falla, ok=False y error explica.
|
||||
"""
|
||||
try:
|
||||
import pymeshlab as ml
|
||||
import trimesh
|
||||
except ImportError as exc:
|
||||
return _err(0, 0.0, f"falta dependencia: {exc}. Instala en el venv del "
|
||||
f"registry: cd python && uv add trimesh pymeshlab scipy")
|
||||
|
||||
if not os.path.exists(in_path):
|
||||
return _err(0, 0.0, f"no existe el archivo de entrada: {in_path!r}")
|
||||
if out_path is None:
|
||||
out_path = os.path.splitext(in_path)[0] + "_simplified.glb"
|
||||
|
||||
in_mb = round(os.path.getsize(in_path) / 1e6, 3)
|
||||
try:
|
||||
orig = _load_mesh(in_path)
|
||||
except Exception as exc:
|
||||
return _err(0, in_mb, f"no se pudo cargar la malla {in_path!r}: {exc}")
|
||||
|
||||
in_faces = int(len(orig.faces))
|
||||
kind, attr, image = _detect_appearance(orig)
|
||||
|
||||
try:
|
||||
ms = ml.MeshSet()
|
||||
if kind == "vertex_color":
|
||||
ms.add_mesh(ml.Mesh(
|
||||
vertex_matrix=orig.vertices, face_matrix=orig.faces,
|
||||
v_color_matrix=attr.astype(np.float64) / 255.0,
|
||||
))
|
||||
else:
|
||||
ms.add_mesh(ml.Mesh(vertex_matrix=orig.vertices, face_matrix=orig.faces))
|
||||
|
||||
if weld:
|
||||
ms.apply_filter("meshing_remove_duplicate_vertices")
|
||||
ms.apply_filter("meshing_remove_unreferenced_vertices")
|
||||
welded_faces = int(ms.current_mesh().face_number())
|
||||
|
||||
decimate_status = "ok"
|
||||
if target_faces < welded_faces:
|
||||
ms.apply_filter(
|
||||
"meshing_decimation_quadric_edge_collapse",
|
||||
targetfacenum=int(target_faces), qualitythr=0.3,
|
||||
preserveboundary=True, boundaryweight=3.0,
|
||||
preservenormal=True, preservetopology=True, autoclean=True,
|
||||
)
|
||||
else:
|
||||
decimate_status = f"skipped_target_ge_welded({welded_faces})"
|
||||
|
||||
cm = ms.current_mesh()
|
||||
dec = trimesh.Trimesh(
|
||||
vertices=cm.vertex_matrix(), faces=cm.face_matrix(), process=False,
|
||||
)
|
||||
|
||||
appearance = kind
|
||||
if kind == "vertex_color" and cm.has_vertex_color():
|
||||
cols = np.clip(cm.vertex_color_matrix() * 255.0, 0, 255).astype(np.uint8)
|
||||
dec.visual = trimesh.visual.color.ColorVisuals(mesh=dec, vertex_colors=cols)
|
||||
elif kind == "texture":
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
_, idx = cKDTree(orig.vertices).query(dec.vertices)
|
||||
dec.visual = trimesh.visual.texture.TextureVisuals(uv=attr[idx], image=image)
|
||||
|
||||
parent = os.path.dirname(out_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
dec.export(out_path)
|
||||
except Exception as exc:
|
||||
return _err(in_faces, in_mb, f"fallo al decimar/exportar: {type(exc).__name__}: {exc}")
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"in_faces": in_faces,
|
||||
"out_faces": int(len(dec.faces)),
|
||||
"in_mb": in_mb,
|
||||
"out_mb": round(os.path.getsize(out_path) / 1e6, 3),
|
||||
"out_path": out_path,
|
||||
"welded_faces": welded_faces,
|
||||
"decimate_status": decimate_status,
|
||||
"appearance": appearance,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
src = sys.argv[1] if len(sys.argv) > 1 else (
|
||||
os.path.expanduser("~/ComfyUI/output/character_3d_00001_.glb"))
|
||||
tgt = int(sys.argv[2]) if len(sys.argv) > 2 else 80000
|
||||
out = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
print(json.dumps(comfyui_simplify_mesh(src, target_faces=tgt, out_path=out), indent=2))
|
||||
Reference in New Issue
Block a user