Ronaldo-GOAT's picture
Add files using upload-large-folder tool
09df272 verified
Raw History Blame Contribute Delete
6.78 kB
"""Self-test for the appearance+geometry harness. MUST pass before use.
Checks:
(1) ORACLE : GT mesh vs itself -> CD~0, F@all~1.0, VolIoU~1.0,
LPIPS~0, SSIM~1, CLIP~1, PSNR very high.
(2) RENDER : render a VERTEX-COLORED glb (primary path for our methods,
and toys4k GT) from the input view; confirm alpha mask is
sane, mask IoU vs stored depth mask is high, and rendered
colours are NOT flat grey (real colour variance). Also
render a UV-textured glb if available to exercise that path.
Proof sheets saved to .debug/appeval/.
(3) MONOTONIC : a noise-perturbed mesh scores WORSE than the oracle on
EVERY metric (CD up, F down, IoU down, NC down; LPIPS up,
SSIM down, CLIP down, PSNR down).
Runs on 2 real toys4k objects using their GT canon.glb as the "prediction".
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import numpy as np
import trimesh
from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parent))
import render as R
from geometry import geometry_metrics
from appearance import appearance_metrics, composite_white
EXP = Path("/lp-dev/jonghoon/mv-mesh/exp_faithfulness/toys4k")
DEBUG = Path("/home/nvidia/jonghoon/mv-mesh/.debug/appeval")
OBJECTS = ["apple_002", "car_006"]
UV_GLB = EXP / "gen_2v" / "trellis" / "apple_002.glb" # optional UV path
def perturb(mesh: trimesh.Trimesh, sigma=0.03, seed=0):
m = mesh.copy()
rng = np.random.default_rng(seed)
m.vertices = m.vertices + rng.normal(0, sigma, size=m.vertices.shape)
return m
def mask_iou(a, b):
a = a.astype(bool); b = b.astype(bool)
return float((a & b).sum() / max((a | b).sum(), 1))
def check_object(obj):
print(f"\n=== object: {obj} ===")
gt_path = EXP / "renders" / f"{obj}_canon.glb"
gt = trimesh.load(str(gt_path), force="mesh", process=False)
gt_gl = R.prepare_mesh(gt)
assert gt_gl.mode == "vertex", f"GT should be vertex-coloured, got {gt_gl.mode}"
# ---- (2) render vertex-coloured mesh from input view ----
z = np.load(EXP / "renders" / f"{obj}_front.npz")
K = {k: float(z[k]) for k in ("fx", "fy", "cx", "cy")}
res = int(z["res"])
c2w = z["c2w_cv"]
depth_mask = z["depth_mm"] > 0
rgba = R.render_input_view(gt_gl, K, c2w, res, res).cpu().numpy()
alpha = rgba[..., 3] > 0.5
iou = mask_iou(alpha, depth_mask)
obj_rgb = rgba[..., :3][alpha]
col_std = float(obj_rgb.std(0).mean())
col_mean = obj_rgb.mean(0)
print(f" [render] mask IoU vs depth = {iou:.4f} | "
f"colour mean={np.round(col_mean,3).tolist()} std={col_std:.4f}")
assert iou > 0.9, f"input-view mask IoU too low ({iou})"
assert alpha.sum() > 1000, "empty render"
assert col_std > 0.02, f"render looks flat grey (col_std={col_std}); " \
"vertex colours not applied!"
assert not np.allclose(col_mean, [0.6, 0.6, 0.6], atol=0.02), \
"render is the flat-grey fallback colour"
DEBUG.mkdir(parents=True, exist_ok=True)
Image.fromarray((composite_white(rgba) * 255).astype(np.uint8)).save(
DEBUG / f"selfcheck_{obj}_vertexcolor.png")
# a couple orbit views for the proof sheet
cams = R.orbit_cameras()
orb = R.render_orbit(gt_gl, cams).cpu().numpy()
sheet = np.concatenate([composite_white(orb[i]) for i in (0, 4, 8, 12, 16, 20)], axis=1)
Image.fromarray((sheet * 255).astype(np.uint8)).save(
DEBUG / f"selfcheck_{obj}_orbit.png")
# ---- (1) ORACLE geometry ----
go = geometry_metrics(gt, gt)
print(f" [oracle-geom] CD_L1={go['cd_l1']:.5f} CD_L2={go['cd_l2']:.6f} "
f"F@.01={go['f01']:.4f} F@.02={go['f02']:.4f} F@.05={go['f05']:.4f} "
f"NC={go['normal_consistency']:.4f} VolIoU={go['vol_iou']:.4f}")
assert go["cd_l1"] < 5e-3, go["cd_l1"]
assert go["f01"] > 0.99 and go["f02"] > 0.99 and go["f05"] > 0.99
assert go["normal_consistency"] > 0.99
assert go["vol_iou"] > 0.99
# ---- (1) ORACLE appearance (identical renders) ----
ao = appearance_metrics(rgba, rgba)
print(f" [oracle-app ] LPIPS={ao['lpips']:.5f} SSIM={ao['ssim']:.4f} "
f"CLIP={ao['clip']:.4f} PSNR={ao['psnr']:.2f}")
assert ao["lpips"] < 1e-4
assert ao["ssim"] > 0.999
assert ao["clip"] > 0.999
# ---- (3) MONOTONICITY: perturbed mesh worse on every metric ----
pert = perturb(gt, sigma=0.03)
pert_gl = R.prepare_mesh(pert)
gp = geometry_metrics(pert, gt)
pert_rgba = R.render_input_view(pert_gl, K, c2w, res, res).cpu().numpy()
ap = appearance_metrics(pert_rgba, rgba)
print(f" [perturbed ] CD_L1={gp['cd_l1']:.5f} F@.02={gp['f02']:.4f} "
f"VolIoU={gp['vol_iou']:.4f} NC={gp['normal_consistency']:.4f} | "
f"LPIPS={ap['lpips']:.4f} SSIM={ap['ssim']:.4f} CLIP={ap['clip']:.4f} "
f"PSNR={ap['psnr']:.2f}")
checks = {
"CD_L1 up": gp["cd_l1"] > go["cd_l1"],
"CD_L2 up": gp["cd_l2"] > go["cd_l2"],
"F@.01 down": gp["f01"] < go["f01"],
"F@.02 down": gp["f02"] < go["f02"],
"F@.05 down": gp["f05"] < go["f05"],
"NC down": gp["normal_consistency"] < go["normal_consistency"],
"VolIoU down": gp["vol_iou"] < go["vol_iou"],
"LPIPS up": ap["lpips"] > ao["lpips"],
"SSIM down": ap["ssim"] < ao["ssim"],
"CLIP down": ap["clip"] < ao["clip"],
"PSNR down": ap["psnr"] < ao["psnr"],
}
for name, ok in checks.items():
assert ok, f"MONOTONICITY FAIL: {name}"
print(f" [monotonic ] all {len(checks)} metrics degrade correctly: PASS")
return go, ao
def check_uv_path():
if not UV_GLB.exists():
print("\n[uv-path] no UV-textured glb available, skipping")
return
m = trimesh.load(str(UV_GLB), force="mesh", process=False)
g = R.prepare_mesh(m)
print(f"\n[uv-path] {UV_GLB.name} -> mode={g.mode}")
cams = R.orbit_cameras()
r = R.render_orbit(g, cams[:1]).cpu().numpy()[0]
obj = r[..., 3] > 0.5
assert obj.sum() > 500, "UV render empty"
col_std = float(r[..., :3][obj].std(0).mean())
print(f" UV render obj px={int(obj.sum())} colour std={col_std:.4f} "
f"(mode={g.mode})")
Image.fromarray((composite_white(r) * 255).astype(np.uint8)).save(
DEBUG / "selfcheck_uvpath.png")
def main():
print("APPEVAL SELF-CHECK")
for obj in OBJECTS:
check_object(obj)
check_uv_path()
print("\n==================================================")
print("ALL SELF-CHECKS PASSED")
print("proof sheets ->", DEBUG)
print("==================================================")
if __name__ == "__main__":
main()