ShadeNet-5 / test_space.py
singam96's picture
Add the small refiner as the recommended mode
0c2e662 verified
Raw History Blame Contribute Delete
4.45 kB
"""Check the Space's copied averaging code against the repository's original.
Run from the repo root inside the project container:
docker compose run --rm train bash -c "pip install -q onnxruntime gradio huggingface_hub && python space/test_space.py"
"""
import os
import sys
import time
import torch
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
sys.path.insert(0, HERE)
os.chdir(HERE)
import app # noqa: E402
import ensemble # noqa: E402 (downloads config.json + the MIX ONNX from the Hub)
sys.path.insert(0, REPO)
from detail_diffuser.inference.core import tta_average as ref_tta, unit_normals as ref_unit # noqa: E402
img = Image.open(os.path.join(HERE, "examples", "3185371756_ff4e9fa8a6.jpg"))
from framing import letterbox # noqa: E402
sq, box = letterbox(img, app.SIZE)
x = app.to_tensor(sq)
assert x.shape == (1, 3, app.SIZE, app.SIZE) and x.min() >= -1 and x.max() <= 1
assert box[2] == app.SIZE or box[3] == app.SIZE, box # the long side fills the square
t = time.time()
fast = app.refine(x)
print(f"fast: {time.time() - t:.1f}s, maps {tuple(fast.shape)}")
assert fast.shape == (1, 7, app.SIZE, app.SIZE) and torch.isfinite(fast).all()
# The exported graph samples its input-perturbation noise on every call, so
# two runs of the real model differ slightly (mean ~1e-3). The averaging code
# is therefore compared with a deterministic stand-in model, which must match
# the repository's implementation exactly.
def stand_in(c):
return torch.cat([c * 0.7, c.mean(1, keepdim=True) * 0.5, c.flip(-1) * 0.3], 1)
mine = app.unit_normals(ensemble.tta_average(stand_in, x, flip=True, rot_deg=app.ROT_DEG))
ref = ref_unit(ref_tta(lambda c, nz=None: stand_in(c), x, flip=True, rot_deg=app.ROT_DEG))
d = float((mine - ref).abs().max())
print("averaging code, max |space - repo| =", d)
assert d == 0.0, d
e1 = app.unit_normals(ensemble.tta_average(app.refine, x, flip=True, rot_deg=app.ROT_DEG))
assert e1.shape == (1, 7, app.SIZE, app.SIZE) and torch.isfinite(e1).all()
n = e1[:, 4:7].norm(dim=1)
assert float((n - 1).abs().max()) < 1e-4, "normals are not unit length"
print("real model, 6 passes ok, mean |maps| =", float(e1.abs().mean()))
# median_of against NumPy's nanmedian on real stand-in passes (corners are covered by fewer passes)
import numpy as np # noqa: E402
passes = app.tta_passes(lambda c: stand_in(c) + 0.05 * torch.randn(1, 7, app.SIZE, app.SIZE),
x, flip=True, rot_deg=app.ROT_DEG)
assert len(passes) == 6 and any(float(v.min()) < 0.5 for _, v in passes), "rotated passes should have uncovered corners"
P = np.stack([p.numpy() for p, _ in passes]).copy()
V = np.stack([v.numpy() for _, v in passes]) > 0.5
P[~np.broadcast_to(V, P.shape)] = np.nan
want = np.nanmedian(P, axis=0)
got = app.median_of(passes).numpy()
dm = float(np.abs(got - want).max())
print("median_of vs numpy nanmedian, max diff =", dm)
assert dm < 1e-5, dm
assert float((app.median_of(passes) - app.mean_of(passes)).abs().mean()) > 1e-4, "median should differ from mean on noisy passes"
one = [passes[0]]
assert torch.equal(app.median_of(one), app.mean_of(one)), "one pass: median == mean"
out = app.predict(img, app.MODE_SIX, False)
assert len(out) == 6 and out[0].size == out[1].size == img.size, (out[0].size, img.size) # whole photo, own shape
assert out[4] is None and "(mean)" in out[5]
print("predict ok:", out[5])
out = app.predict(img, app.MODE_SIX, True)
assert out[4] is not None and out[4].size == img.size and "(median)" in out[5], out[5]
print("median ok:", out[5])
out = app.predict(img, app.MODE_SINGLE, True) # median outside the six-pass mode: single pass, note says so
assert out[4] is None and "1 forward pass in" in out[5] and "six-pass mode" in out[5], out[5]
out = app.predict(img, app.MODE_REFINE, False) # single pass + the small refiner
assert out[4] is None and "refiner" in out[5] and out[1].size == img.size, out[5]
print("refiner mode ok:", out[5])
# odd shapes: a panorama (padding longer than the photo), tall, square, tiny, big
for size in [(2000, 300), (300, 2000), (448, 448), (64, 48), (1500, 1100)]:
im = Image.effect_noise(size, 40).convert("RGB")
out = app.predict(im, app.MODE_REFINE, False)
ow, oh = app.display_size(*size)
assert out[0].size == out[1].size == out[2].size == out[3].size == (ow, oh), (size, out[0].size, (ow, oh))
print("odd shapes ok")
print("space test ok")