some sort of progress bar for cgal
This commit is contained in:
@@ -5,26 +5,76 @@
|
||||
Alpha Wrap Node - Shrink wrap mesh generation using CGAL's Alpha Wrap algorithm.
|
||||
|
||||
Creates a watertight mesh that tightly wraps around input geometry.
|
||||
Useful for point clouds, non-manifold meshes, or polygon soups.
|
||||
Useful for non-manifold meshes or polygon soups.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import trimesh
|
||||
import comfy.utils
|
||||
from comfy_api.latest import io
|
||||
|
||||
from CGAL import CGAL_Alpha_wrap_3
|
||||
from CGAL.CGAL_Kernel import Point_3
|
||||
from CGAL.CGAL_Polyhedron_3 import Polyhedron_3
|
||||
|
||||
log = logging.getLogger("geometrypack")
|
||||
|
||||
# Progress bar layout (percentage of total)
|
||||
_PHASE_BUILD = 15 # 0-15%: building CGAL input
|
||||
_PHASE_WRAP_START = 15 # 15%: wrap starts
|
||||
_PHASE_WRAP_END = 85 # ..85%: wrap ends (time-based fill)
|
||||
_PHASE_EXTRACT = 100 # 85-100%: extracting result
|
||||
|
||||
|
||||
def _get_rss_mb():
|
||||
"""Get current process RSS in MB (Linux)."""
|
||||
try:
|
||||
with open(f"/proc/{os.getpid()}/statm", "r") as f:
|
||||
pages = int(f.read().split()[1])
|
||||
return pages * os.sysconf("SC_PAGE_SIZE") / (1024 * 1024)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _polyhedron_to_arrays(poly):
|
||||
"""Extract vertices and faces from a CGAL Polyhedron_3."""
|
||||
verts = []
|
||||
vertex_map = {}
|
||||
for i, vertex in enumerate(poly.vertices()):
|
||||
point = vertex.point()
|
||||
verts.append([float(point.x()), float(point.y()), float(point.z())])
|
||||
vertex_map[vertex] = i
|
||||
|
||||
faces = []
|
||||
for facet in poly.facets():
|
||||
halfedge = facet.halfedge()
|
||||
face_verts = []
|
||||
start_he = halfedge
|
||||
current = start_he
|
||||
while True:
|
||||
face_verts.append(vertex_map[current.vertex()])
|
||||
current = current.next()
|
||||
if current == start_he:
|
||||
break
|
||||
if len(face_verts) == 3:
|
||||
faces.append(face_verts)
|
||||
|
||||
return np.array(verts, dtype=np.float64), np.array(faces, dtype=np.int32)
|
||||
|
||||
|
||||
class AlphaWrapNode(io.ComfyNode):
|
||||
"""
|
||||
Alpha Wrap - Generate a watertight shrink-wrapped mesh.
|
||||
|
||||
Uses CGAL's Alpha Wrap algorithm (via PyMeshLab) to create a
|
||||
tight-fitting watertight mesh around input geometry.
|
||||
Uses CGAL's Alpha Wrap algorithm to create a tight-fitting
|
||||
watertight mesh around input geometry.
|
||||
|
||||
Works with:
|
||||
- Point clouds
|
||||
- Non-manifold meshes
|
||||
- Meshes with holes
|
||||
- Polygon soups (overlapping/intersecting geometry)
|
||||
@@ -46,8 +96,8 @@ class AlphaWrapNode(io.ComfyNode):
|
||||
is_output_node=True,
|
||||
inputs=[
|
||||
io.Custom("TRIMESH").Input("input_mesh"),
|
||||
io.Float.Input("alpha_percent", default=0.04, min=0.001, max=50.0, step=0.01, tooltip="Wrap tightness as % of bounding box diagonal. Smaller = tighter wrap with more detail, but slower.", optional=True),
|
||||
io.Float.Input("offset_percent", default=1.1, min=0.01, max=10.0, step=0.01, tooltip="Surface offset as % of bounding box diagonal. Smaller = closer to original surface.", optional=True),
|
||||
io.Float.Input("alpha_percent", default=2.0, min=0.001, max=50.0, step=0.1, tooltip="Wrap tightness as % of bounding box diagonal. Smaller = tighter wrap with more detail, but MUCH slower (runtime ~ 1/alpha\u00b3). Start at 2-5% and tighten if needed.", optional=True),
|
||||
io.Float.Input("offset_percent", default=2.0, min=0.01, max=10.0, step=0.1, tooltip="Surface offset as % of bounding box diagonal. Smaller = closer to original surface.", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("TRIMESH").Output(display_name="wrapped_mesh"),
|
||||
@@ -56,31 +106,10 @@ class AlphaWrapNode(io.ComfyNode):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, input_mesh, alpha_percent=0.04, offset_percent=1.1):
|
||||
"""
|
||||
Generate alpha-wrapped mesh.
|
||||
|
||||
Args:
|
||||
input_mesh: Input trimesh.Trimesh object (mesh or point cloud)
|
||||
alpha_percent: Alpha value as percentage of bbox diagonal
|
||||
offset_percent: Offset value as percentage of bbox diagonal
|
||||
|
||||
Returns:
|
||||
tuple: (wrapped_trimesh, report_string)
|
||||
"""
|
||||
try:
|
||||
import pymeshlab
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Alpha Wrap requires PyMeshLab with CGAL support.\n"
|
||||
"Install with: pip install pymeshlab"
|
||||
)
|
||||
|
||||
# Get input stats
|
||||
def execute(cls, input_mesh, alpha_percent=2.0, offset_percent=2.0):
|
||||
vertices = np.asarray(input_mesh.vertices, dtype=np.float64)
|
||||
input_vertex_count = len(vertices)
|
||||
|
||||
# Check if input is point cloud or mesh
|
||||
is_point_cloud = (
|
||||
not hasattr(input_mesh, 'faces') or
|
||||
input_mesh.faces is None or
|
||||
@@ -111,30 +140,69 @@ class AlphaWrapNode(io.ComfyNode):
|
||||
log.info("Alpha: %.6f (%s%% of bbox)", alpha, alpha_percent)
|
||||
log.info("Offset: %.6f (%s%% of bbox)", offset, offset_percent)
|
||||
|
||||
# Create PyMeshLab MeshSet
|
||||
ms = pymeshlab.MeshSet()
|
||||
pbar = comfy.utils.ProgressBar(100)
|
||||
|
||||
# Add mesh to MeshSet
|
||||
faces = np.asarray(input_mesh.faces, dtype=np.int32)
|
||||
pml_mesh = pymeshlab.Mesh(
|
||||
vertex_matrix=vertices,
|
||||
face_matrix=faces
|
||||
)
|
||||
ms.add_mesh(pml_mesh)
|
||||
# --- Phase 1: Build CGAL input data (0-15%) ---
|
||||
t0 = time.monotonic()
|
||||
cgal_points = CGAL_Alpha_wrap_3.Point_3_Vector()
|
||||
cgal_points.reserve(input_vertex_count)
|
||||
report_every = max(1, input_vertex_count // 10)
|
||||
for i, v in enumerate(vertices):
|
||||
cgal_points.append(Point_3(float(v[0]), float(v[1]), float(v[2])))
|
||||
if i % report_every == 0:
|
||||
pbar.update_absolute(int(_PHASE_BUILD * i / input_vertex_count))
|
||||
|
||||
# Run Alpha Wrap
|
||||
log.info("Running CGAL Alpha Wrap... (this may take a while)")
|
||||
ms.generate_alpha_wrap(
|
||||
alpha=pymeshlab.PureValue(alpha),
|
||||
offset=pymeshlab.PureValue(offset)
|
||||
)
|
||||
cgal_faces = [[int(idx) for idx in face] for face in input_mesh.faces]
|
||||
pbar.update_absolute(_PHASE_BUILD)
|
||||
log.info("Built CGAL input data in %.1fs", time.monotonic() - t0)
|
||||
|
||||
# Get result
|
||||
result_pml = ms.current_mesh()
|
||||
result_vertices = result_pml.vertex_matrix()
|
||||
result_faces = result_pml.face_matrix()
|
||||
# --- Phase 2: Run alpha wrap (15-85%) ---
|
||||
output_poly = Polyhedron_3()
|
||||
exc_holder = [None]
|
||||
rss_before = _get_rss_mb()
|
||||
|
||||
def _run():
|
||||
try:
|
||||
CGAL_Alpha_wrap_3.alpha_wrap_3(cgal_points, cgal_faces, alpha, offset, output_poly)
|
||||
except Exception as e:
|
||||
exc_holder[0] = e
|
||||
|
||||
log.info("Running CGAL Alpha Wrap...")
|
||||
thread = threading.Thread(target=_run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
wrap_start = time.monotonic()
|
||||
interval = 5
|
||||
wrap_range = _PHASE_WRAP_END - _PHASE_WRAP_START
|
||||
while thread.is_alive():
|
||||
thread.join(timeout=interval)
|
||||
elapsed = time.monotonic() - wrap_start
|
||||
rss = _get_rss_mb()
|
||||
delta = rss - rss_before
|
||||
if thread.is_alive():
|
||||
# Asymptotic progress: approaches _PHASE_WRAP_END but never reaches it
|
||||
# Uses 1 - 1/(1 + t/30) so it fills ~50% at 30s, ~75% at 90s, etc.
|
||||
frac = 1.0 - 1.0 / (1.0 + elapsed / 30.0)
|
||||
progress = int(_PHASE_WRAP_START + wrap_range * frac)
|
||||
pbar.update_absolute(progress)
|
||||
log.info("Alpha Wrap running... %.0fs elapsed, RSS %.0f MB (+%.0f MB)",
|
||||
elapsed, rss, delta)
|
||||
|
||||
elapsed_total = time.monotonic() - wrap_start
|
||||
rss_final = _get_rss_mb()
|
||||
pbar.update_absolute(_PHASE_WRAP_END)
|
||||
log.info("Alpha Wrap finished in %.1fs, RSS %.0f MB (+%.0f MB)",
|
||||
elapsed_total, rss_final, rss_final - rss_before)
|
||||
|
||||
if exc_holder[0] is not None:
|
||||
raise exc_holder[0]
|
||||
|
||||
# --- Phase 3: Extract result mesh (85-100%) ---
|
||||
t0 = time.monotonic()
|
||||
result_vertices, result_faces = _polyhedron_to_arrays(output_poly)
|
||||
pbar.update_absolute(_PHASE_EXTRACT)
|
||||
log.info("Extracted result mesh in %.1fs", time.monotonic() - t0)
|
||||
|
||||
# Create trimesh result
|
||||
result_mesh = trimesh.Trimesh(
|
||||
vertices=result_vertices,
|
||||
faces=result_faces,
|
||||
@@ -147,7 +215,6 @@ class AlphaWrapNode(io.ComfyNode):
|
||||
else:
|
||||
result_mesh.metadata = {}
|
||||
|
||||
# Add alpha wrap info to metadata
|
||||
result_mesh.metadata['alpha_wrap'] = {
|
||||
'alpha': alpha,
|
||||
'alpha_percent': alpha_percent,
|
||||
@@ -157,7 +224,6 @@ class AlphaWrapNode(io.ComfyNode):
|
||||
'input_type': input_type
|
||||
}
|
||||
|
||||
# Compute stats
|
||||
output_vertex_count = len(result_mesh.vertices)
|
||||
output_face_count = len(result_mesh.faces)
|
||||
is_watertight = result_mesh.is_watertight
|
||||
@@ -165,7 +231,6 @@ class AlphaWrapNode(io.ComfyNode):
|
||||
log.info("Result: %s vertices, %s faces", f"{output_vertex_count:,}", f"{output_face_count:,}")
|
||||
log.info("Watertight: %s", is_watertight)
|
||||
|
||||
# Build report
|
||||
report = f"""Alpha Wrap Report
|
||||
{'='*40}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user