FORGE3DBench: final eval protocol + eval_final.py, batched Ours inference, held-out view tars, missing-object lists, README
cca6827 verified Download forgebench/code/eval/appeval/geometry.py from Ronaldo-GOAT/bert_simpson: direct link, hf CLI and curl.
- Browser
- Download file 5.13 kB
-
https://huggingface.co/Ronaldo-GOAT/bert_simpson/resolve/main/forgebench/code/eval/appeval/geometry.py
- Command line
-
hf download hf://Ronaldo-GOAT/bert_simpson/forgebench/code/eval/appeval/geometry.py
-
curl -L -o geometry.py https://huggingface.co/Ronaldo-GOAT/bert_simpson/resolve/main/forgebench/code/eval/appeval/geometry.py
5.13 kB
| """Geometry metrics vs a GT mesh (LOCKED spec). | |
| Both meshes are already canonical in [-0.5,0.5]^3; NO ICP / NO alignment. | |
| 100k surface points sampled per mesh (same count both sides) via | |
| pytorch3d.ops.sample_points_from_meshes. NN distances via knn_points. | |
| Reported: | |
| * Chamfer L1 (headline): mean over both directions of the sqrt Euclidean | |
| nearest-neighbour distance. Units = fraction of the unit-cube edge. | |
| * Chamfer L2 : pytorch3d.loss.chamfer_distance default (mean squared). | |
| * F-score@{0.01,0.02,0.05}: P = frac pred within tau of GT, | |
| R = frac GT within tau of pred, F = 2PR/(P+R). Headline F@0.02. | |
| * Normal Consistency: mean abs cosine of normals at mutual NN (both dirs). | |
| * Volume IoU@128^3: solid voxel occupancy on a shared 128^3 grid, I/U. | |
| """ | |
| from __future__ import annotations | |
| # FINAL: = metrics/appeval/geometry.py + extent-relative CD/F keys, optional vol IoU. Original keys unchanged. | |
| import numpy as np | |
| import torch | |
| import trimesh | |
| from pytorch3d.structures import Meshes | |
| from pytorch3d.ops import sample_points_from_meshes, knn_points | |
| from pytorch3d.loss import chamfer_distance | |
| N_SAMPLES = 100_000 | |
| TAUS = (0.01, 0.02, 0.05) | |
| VOX_RES = 128 | |
| def _to_p3d(mesh: trimesh.Trimesh, device): | |
| v = torch.as_tensor(np.asarray(mesh.vertices), dtype=torch.float32, device=device) | |
| f = torch.as_tensor(np.asarray(mesh.faces), dtype=torch.int64, device=device) | |
| return Meshes(verts=[v], faces=[f]) | |
| def _sample(mesh, device, n=N_SAMPLES, seed=0): | |
| torch.manual_seed(seed) | |
| m = _to_p3d(mesh, device) | |
| pts, nrm = sample_points_from_meshes(m, n, return_normals=True) | |
| return pts, nrm # (1,n,3), (1,n,3) | |
| def _voxelize_solid(mesh: trimesh.Trimesh, res=VOX_RES): | |
| """Solid occupancy on a fixed res^3 grid over [-0.5,0.5]^3 (bool array).""" | |
| pitch = 1.0 / res | |
| grid = np.zeros((res, res, res), dtype=bool) | |
| try: | |
| vox = mesh.voxelized(pitch=pitch) | |
| try: | |
| vox = vox.fill() | |
| except Exception: | |
| pass # non-watertight -> hollow occupancy, still comparable | |
| pts = np.asarray(vox.points) # world-space centres of occupied cells | |
| except Exception: | |
| # last-resort: dense surface samples | |
| pts, _ = trimesh.sample.sample_surface(mesh, 2_000_000, seed=0) | |
| pts = np.asarray(pts) | |
| idx = np.floor((pts + 0.5) * res).astype(np.int64) | |
| idx = np.clip(idx, 0, res - 1) | |
| grid[idx[:, 0], idx[:, 1], idx[:, 2]] = True | |
| return grid | |
| def geometry_metrics(pred_mesh: trimesh.Trimesh, gt_mesh: trimesh.Trimesh, | |
| device="cuda", seed=0, with_vol=True): | |
| """Return a dict of all geometry metrics. NO alignment applied.""" | |
| pp, pn = _sample(pred_mesh, device, seed=seed) | |
| gp, gn = _sample(gt_mesh, device, seed=seed) | |
| # NN both directions (knn returns squared dists) | |
| p2g = knn_points(pp, gp, K=1) | |
| g2p = knn_points(gp, pp, K=1) | |
| d_p2g = p2g.dists[..., 0].clamp_min(0).sqrt()[0] # pred->gt (n,) | |
| d_g2p = g2p.dists[..., 0].clamp_min(0).sqrt()[0] # gt->pred (n,) | |
| cd_l1 = 0.5 * (d_p2g.mean() + d_g2p.mean()) | |
| cd_l2, _ = chamfer_distance(pp, gp) # default mean squared | |
| fscore = {} | |
| for tau in TAUS: | |
| precision = (d_p2g < tau).float().mean() | |
| recall = (d_g2p < tau).float().mean() | |
| f = (2 * precision * recall / (precision + recall + 1e-12)) | |
| fscore[tau] = {"precision": float(precision), "recall": float(recall), | |
| "f": float(f)} | |
| # normal consistency at mutual NN, both directions | |
| idx_p2g = p2g.idx[..., 0][0] # for each pred pt, nearest gt idx | |
| idx_g2p = g2p.idx[..., 0][0] | |
| gn0, pn0 = gn[0], pn[0] | |
| cos_p = (pn0 * gn0[idx_p2g]).sum(-1).abs() | |
| cos_g = (gn0 * pn0[idx_g2p]).sum(-1).abs() | |
| nc = 0.5 * (cos_p.mean() + cos_g.mean()) | |
| # volume IoU on shared grid | |
| iou = float("nan") | |
| if with_vol: # optional (slow; meaningless for non-watertight GT, see memory uv-flip note) | |
| gp_vox = _voxelize_solid(pred_mesh) | |
| gg_vox = _voxelize_solid(gt_mesh) | |
| inter = np.logical_and(gp_vox, gg_vox).sum() | |
| union = np.logical_or(gp_vox, gg_vox).sum() | |
| iou = float(inter) / float(union) if union > 0 else 0.0 | |
| # ADDED: extent-relative metrics so FB150 (GT max extent 0.9) is comparable with | |
| # Toys/Omni (1.0): distances divided by the GT max bbox extent (tau scaled by it). | |
| ext = float((np.asarray(gt_mesh.vertices).max(0) - np.asarray(gt_mesh.vertices).min(0)).max()) | |
| rel = {} | |
| for tau in TAUS: | |
| t = tau * ext | |
| pr_, rc_ = (d_p2g < t).float().mean(), (d_g2p < t).float().mean() | |
| rel[tau] = float(2 * pr_ * rc_ / (pr_ + rc_ + 1e-12)) | |
| return { | |
| "gt_extent": ext, | |
| "cd_l1_rel": float(cd_l1) / ext, | |
| "f01_rel": rel[0.01], "f02_rel": rel[0.02], "f05_rel": rel[0.05], | |
| "cd_l1": float(cd_l1), | |
| "cd_l2": float(cd_l2), | |
| "fscore": {str(t): fscore[t] for t in TAUS}, | |
| "f01": fscore[0.01]["f"], | |
| "f02": fscore[0.02]["f"], # headline | |
| "f05": fscore[0.05]["f"], | |
| "normal_consistency": float(nc), | |
| "vol_iou": iou, | |
| } | |