Spaces:
Running
Running
| """Quantitative mesh quality / similarity metrics (all verified on this venv). | |
| Two modes: | |
| - vs known target dimensions (always available): OBB-based dimension match. | |
| - vs a reference mesh (GSO etc.): Chamfer distance + volumetric IoU after | |
| normalization and point-to-point ICP alignment. | |
| Pip-only (trimesh + numpy + scipy). No GL, no sudo, no rtree. | |
| Key correctness notes (verified): | |
| - Use the ORIENTED bounding box (OBB) for dimensions; the axis-aligned box | |
| inflates under rotation. | |
| - ICP must be point-to-point (mesh-based ICP needs rtree, which isn't installed). | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import trimesh | |
| from scipy.spatial import cKDTree | |
| def _load(mesh) -> trimesh.Trimesh: | |
| return trimesh.load(mesh, force="mesh") if isinstance(mesh, str) else mesh | |
| def _obb_extents(m: trimesh.Trimesh) -> np.ndarray: | |
| """Orientation-invariant real-world dimensions (sorted ascending).""" | |
| return np.sort(m.bounding_box_oriented.primitive.extents) | |
| # -------------------------------------------------------------------------- | |
| # vs target dimensions (mm) — cheap, always available | |
| # -------------------------------------------------------------------------- | |
| def dimension_score(mesh, target_dims_mm: list[float]) -> dict: | |
| """Compare mesh OBB to a target [x,y,z] in mm (order-insensitive).""" | |
| m = _load(mesh) | |
| got = _obb_extents(m) | |
| tgt = np.sort(np.array(target_dims_mm, dtype=float)) | |
| tgt[tgt == 0] = 1e-6 | |
| rel_err = np.abs(got - tgt) / tgt | |
| score = float(np.clip(1 - rel_err.mean(), 0, 1)) | |
| return { | |
| "dims_got_mm": [round(float(x), 1) for x in got], | |
| "dims_target_mm": [round(float(x), 1) for x in tgt], | |
| "axis_ratio": [round(float(x), 2) for x in (got / tgt)], | |
| "dimension_score": round(score, 3), | |
| } | |
| # -------------------------------------------------------------------------- | |
| # vs reference mesh — Chamfer + voxel IoU after normalize + ICP | |
| # -------------------------------------------------------------------------- | |
| def _normalize(m: trimesh.Trimesh) -> trimesh.Trimesh: | |
| m = m.copy() | |
| m.apply_translation(-m.bounding_box.centroid) | |
| s = m.extents.max() | |
| if s > 0: | |
| m.apply_scale(1.0 / s) | |
| return m | |
| def _icp_align(target: trimesh.Trimesh, moving: trimesh.Trimesh, n: int = 4000) -> trimesh.Trimesh: | |
| """Point-to-point ICP (no rtree): align `moving` onto `target`.""" | |
| T, _, _ = trimesh.registration.icp(moving.sample(n), target.sample(n), max_iterations=50) | |
| out = moving.copy() | |
| out.apply_transform(T) | |
| return out | |
| def chamfer_distance(a, b, n: int = 5000) -> float: | |
| """Symmetric mean Chamfer distance (normalized scale). Lower = better.""" | |
| pa, pb = a.sample(n), b.sample(n) | |
| d_ab = cKDTree(pb).query(pa)[0] | |
| d_ba = cKDTree(pa).query(pb)[0] | |
| return float(d_ab.mean() + d_ba.mean()) | |
| def voxel_iou(a, b, pitch: float = 0.02) -> float: | |
| """Volumetric IoU on filled voxel grids in a shared frame. Higher = better.""" | |
| pa = a.voxelized(pitch).fill().points | |
| pb = b.voxelized(pitch).fill().points | |
| sa = {tuple(p) for p in np.round(pa / pitch).astype(int)} | |
| sb = {tuple(p) for p in np.round(pb / pitch).astype(int)} | |
| u = len(sa | sb) | |
| return float(len(sa & sb) / u) if u else 0.0 | |
| def compare(mesh, reference=None, target_dims_mm=None) -> dict: | |
| """Run whichever metrics are applicable; return a compact report.""" | |
| out: dict = {} | |
| if target_dims_mm is not None: | |
| out.update(dimension_score(mesh, target_dims_mm)) | |
| if reference is not None: | |
| try: | |
| ref = _normalize(_load(reference)) | |
| gen = _icp_align(ref, _normalize(_load(mesh))) | |
| out["chamfer"] = round(chamfer_distance(ref, gen), 4) | |
| out["voxel_iou"] = round(voxel_iou(ref, gen), 3) | |
| except Exception as e: # noqa: BLE001 | |
| out["reference_error"] = str(e) | |
| return out | |