CloneForge / cloneforge /quality.py
avaliev's picture
Upload 75 files
8f1f637 verified
Raw
History Blame Contribute Delete
3.91 kB
"""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