mind3d-trellis2 / code /eval_shape.py
jamie33's picture
MinD-3D + TRELLIS.2 sub-01 experiments: code, metrics, logs, report
87e9895 verified
Raw History Blame Contribute Delete
3.97 kB
"""Shape metrics against ShapeNetCore.v2.PC15k GT, following MinD-3D's tools/get_cd.py protocol
(2048 points, pc_norm), plus a shared orientation search so methods with different canonical frames
are compared fairly."""
import os
import glob
import json
import argparse
import collections
import numpy as np
import trimesh
from scipy.spatial import cKDTree
from scipy.optimize import linear_sum_assignment
N_POINTS = 2048
FSCORE_TAU = 0.02
def pc_norm(pc):
pc = pc - pc.mean(0)
return pc / (2 * np.max(np.linalg.norm(pc, axis=1)))
def rot_y(deg):
t = np.deg2rad(deg)
c, s = np.cos(t), np.sin(t)
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
UP_MAPS = {
"y_up": np.eye(3),
"z_up": np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]], dtype=np.float64),
}
CANDIDATES = [(up, az, rot_y(az) @ m) for up, m in UP_MAPS.items() for az in range(0, 360, 45)]
def chamfer(a, b):
d_ab = cKDTree(b).query(a)[0]
d_ba = cKDTree(a).query(b)[0]
return np.mean(d_ab ** 2) + np.mean(d_ba ** 2), d_ab, d_ba
def fscore(d_ab, d_ba, tau):
p = np.mean(d_ab < tau)
r = np.mean(d_ba < tau)
return 0.0 if p + r == 0 else 2 * p * r / (p + r)
def emd(a, b):
cost = np.linalg.norm(a[:, None] - b[None], axis=-1)
r, c = linear_sum_assignment(cost)
return cost[r, c].mean()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--gen_dir", required=True)
parser.add_argument("--gt_root", default="/home/hubin/data/ShapeNetCore.v2.PC15k/ShapeNetCore.v2.PC15k")
parser.add_argument("--test_list", default="/home/hubin/data/fMRI-Shape/annotations/core_test_list.txt")
parser.add_argument("--out", required=True)
parser.add_argument("--seed", type=int, default=0)
args = parser.parse_args()
gt_paths = {os.path.basename(p)[:-4]: p for p in glob.glob(f"{args.gt_root}/*/_/*/*.npy")}
ids = [l.strip() for l in open(args.test_list) if l.strip()]
rows = []
for obj in ids:
cat, uid = obj.split("/")
gen = [p for p in glob.glob(os.path.join(args.gen_dir, f"{cat}_{uid}.*")) if p.endswith((".ply", ".obj"))]
if uid not in gt_paths or not gen:
continue
rng = np.random.default_rng(args.seed)
gt = np.load(gt_paths[uid])
gt = pc_norm(gt[rng.choice(len(gt), N_POINTS, replace=False)].astype(np.float64))
mesh = trimesh.load(gen[0], force="mesh")
pts = trimesh.sample.sample_surface(mesh, N_POINTS, seed=args.seed)[0].astype(np.float64)
pts = pc_norm(pts)
best = None
for up, az, R in CANDIDATES:
cd, d_ab, d_ba = chamfer(pts @ R.T, gt)
if best is None or cd < best[0]:
best = (cd, up, az, R, d_ab, d_ba)
cd, up, az, R, d_ab, d_ba = best
rows.append({
"id": obj, "cat": cat, "cd": cd, "emd": emd(pts @ R.T, gt),
"fscore": fscore(d_ab, d_ba, FSCORE_TAU), "up": up, "azimuth": az,
})
print(f"{obj} CD={cd * 1e3:.3f}e-3 EMD={rows[-1]['emd']:.4f} F={rows[-1]['fscore']:.3f} ({up},{az})", flush=True)
by_cat = collections.defaultdict(list)
for r in rows:
by_cat[r["cat"]].append(r)
summary = {
"n": len(rows),
"cd_x1e3": float(np.mean([r["cd"] for r in rows]) * 1e3),
"emd": float(np.mean([r["emd"] for r in rows])),
"fscore@0.02": float(np.mean([r["fscore"] for r in rows])),
"per_category": {c: {"n": len(v), "cd_x1e3": float(np.mean([r["cd"] for r in v]) * 1e3),
"emd": float(np.mean([r["emd"] for r in v]))} for c, v in sorted(by_cat.items())},
"chosen_orientation": collections.Counter(f"{r['up']}/{r['azimuth']}" for r in rows).most_common(),
}
with open(args.out, "w") as f:
json.dump({"summary": summary, "rows": rows}, f, indent=1)
print(json.dumps({k: v for k, v in summary.items() if k != "per_category"}, indent=1))
if __name__ == "__main__":
main()