File size: 10,849 Bytes
b958326 9aabf6a b958326 ed3b86e b958326 35ddc05 b958326 ed3b86e b958326 b4c820e b958326 35ddc05 b958326 35ddc05 b958326 35ddc05 b958326 35ddc05 b958326 35ddc05 b958326 35ddc05 b958326 35ddc05 b958326 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | """K3 sheet - four renders per photo, so quality is looked at instead of argued about.
The acceptance test we never ran: put the teacher's 20-step result next to what our
chain produces for the same photo, and look. Numbers like "lat cos 0.84" are a proxy
for this image, and they were made on cosplay studio photos, not on the photos people
actually drop in.
Four PLYs per input, all through the teacher's own decoder so the only differences are
the ones we care about:
t20 teacher conditions, 20 steps, its own sampler (guidance 3, shift 3) <- truth
t2 teacher conditions, 2 steps, guidance 3 <- cost of fewer steps
s2 student conditions, 2 steps, guidance 3 <- cost of our encoder
s2g student conditions, 2 steps, guidance 1, shift 1 <- what the browser runs
Also written per image: the canvas the student encoder saw (1024^2, black), the alpha
the student matte produced, and the teacher's own prepared image. When a sheet cell is
fog, one of those three says which half broke.
Run it from the k3 driver (stage "sheet"), or directly:
FLIP_WORK=/kaggle/working python3 k3_sheet.py /kaggle/working/eval
"""
import importlib.util
import json
import os
import pathlib
import sys
import tarfile
import time
WORK = pathlib.Path(os.environ.get("FLIP_WORK", "/kaggle/working"))
K3 = WORK / "k3_encoder_distill.py"
IMGDIR = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else str(WORK / "eval"))
OUT = WORK / "k3" / "sheet"
OUT.mkdir(parents=True, exist_ok=True)
# 高斯数决定 decoder 取多少个八叉树点(每点 32 个):65536 -> 2048 点,
# 262144 -> 全部 8192 点。浏览器按 65536 做预算,教师要证明"原厂水平"就得给满。
N_GAUSS = int(os.environ.get("FLIP_SHEET_GAUSS", "65536"))
# 输入分辨率:upstream 的 preprocess_image 用模块级 _CANVAS_SIZE,改它会同时改
# 抠图/裁切/最终画布。DINO patch 数按 size/16 变:1024 -> 4101 token,
# 512 -> 1029 token。flow model 的 cond 是 concat + self-attn,长度不写死;
# 输出高斯数不受影响。只有显式设了 FLIP_SHEET_IN 才去动 upstream 的全局。
IN_SIZE = int(os.environ.get("FLIP_SHEET_IN", "1024"))
os.environ.setdefault("FLIP_WORK", str(WORK))
def p(*a):
print(f"[{time.strftime('%H:%M:%S')}]", *a, flush=True)
def load_k3():
spec = importlib.util.spec_from_file_location("k3", str(K3))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def pad2(f):
import torch # noqa: PLC0415
z = torch.zeros(f.shape[0], 5, f.shape[2], dtype=f.dtype, device=f.device)
return torch.cat([z, f], dim=1)
def parse_variants():
"""tag:cond:steps:guidance:shift, comma separated.
cond is "t" (teacher conditions) or "s" (student conditions). The student was
distilled with the guidance folded into the target, so its contract grid is
g1/shift1; running it at the teacher's g3 is informative but off-contract.
"""
specs = os.environ.get(
"FLIP_SHEET_VARIANTS",
"t20:t:20:3:3,t2:t:2:3:3,s2:s:2:3:3,s2g:s:2:1:1").split(",")
out = []
for spec in specs:
tag, which, steps, gs, shift = [s.strip() for s in spec.split(":")]
out.append((tag, which, int(steps), float(gs), float(shift)))
return out
def stats(g):
return {
"n": int(g.get_xyz.shape[0]),
"opacity": round(float(g.get_opacity.detach().float().mean()), 4),
"scale": round(float(g.get_scaling.detach().float().mean()), 5),
"x": [round(float(g.get_xyz[:, 0].min()), 3),
round(float(g.get_xyz[:, 0].max()), 3)],
}
def main():
import numpy as np # noqa: PLC0415
import torch # noqa: PLC0415
import torch.nn.functional as F # noqa: PLC0415
from PIL import Image # noqa: PLC0415
from torchvision import transforms # noqa: PLC0415
dev = "cuda" if torch.cuda.is_available() else "cpu"
p("device", dev, "torch", torch.__version__)
k3 = load_k3()
sys.path.insert(0, str(k3.REPO))
import triposplat as T # noqa: PLC0415
if IN_SIZE != 1024:
T._CANVAS_SIZE = IN_SIZE
p("input canvas patched ->", IN_SIZE, "(teacher DINO tokens ->",
(IN_SIZE // 16) ** 2 + 5, ")")
imgs = sorted(q for q in IMGDIR.rglob("*")
if q.suffix.lower() in (".png", ".jpg", ".jpeg", ".webp")
and not q.name.startswith("."))
limit = int(os.environ.get("FLIP_SHEET_N", "0"))
if limit:
imgs = imgs[:limit]
if not imgs:
raise SystemExit(f"no images under {IMGDIR}")
p("images", len(imgs), [q.name for q in imgs])
variants = parse_variants()
p("variants", [f"{t}·{c}{s}st·g{g}·sh{sh}" for t, c, s, g, sh in variants])
t0 = time.time()
pipe = k3.build_teacher_pipeline(T, dev)
p("teacher pipeline", round(time.time() - t0, 1), "s")
dino_s = k3.load_weights(k3.build_dino(), "dino", dev)
vae_s = k3.load_weights(k3.build_vae(), "vae", dev)
matte_s = k3.load_weights(k3.build_matte(), "matte", dev)
p("student encoders loaded")
report = []
for i, path in enumerate(imgs):
stem = path.stem
row = {"image": path.name, "stem": stem, "runs": {}}
try:
t_img = time.time()
image = Image.open(path).convert("RGB")
row["size"] = list(image.size)
# --- teacher path: its own preprocess, its own encoder
prepared = pipe.preprocess_image(image)
prepared.save(OUT / f"{stem}_prepared.png")
row["prepared"] = list(prepared.size)
gen = torch.Generator(device=dev).manual_seed(42)
cond_t = pipe.encode_image(prepared, generator=gen)
# --- student path: mirror of site/src/pipeline.js loadCondition
rgb = k3.resize_min_side(image)
w, h = rgb.size
xin = transforms.ToTensor()(rgb).unsqueeze(0).to(dev)
with torch.no_grad():
a = matte_s(xin)[0, 0].float().cpu().numpy()
alpha = (np.clip(a, 0, 1) * 255).astype(np.uint8)
Image.fromarray(alpha, "L").save(OUT / f"{stem}_mask.png")
er = np.array(k3.erode_alpha(alpha, 1), dtype=np.uint8)
box = k3.square_crop_box(er)
rgba = rgb.convert("RGBA")
rgba.putalpha(Image.fromarray(alpha, "L"))
cropped = rgba.crop(box).resize((k3.CANVAS, k3.CANVAS), Image.LANCZOS)
canvas = Image.new("RGB", (k3.CANVAS, k3.CANVAS), (0, 0, 0))
canvas.paste(cropped, mask=cropped.split()[3])
canvas.save(OUT / f"{stem}_canvas.png")
fx = alpha.astype(np.float32) / 255.0
row["mask"] = {
"resized": [w, h],
"box": [int(v) for v in box],
"fill": round(float(fx.mean()), 4),
"coverage": round(float((fx > 0.5).mean()), 4),
}
x = transforms.ToTensor()(canvas).unsqueeze(0).to(dev)
dino_dev = next(pipe.dinov3.parameters()).device
dino_dt = next(pipe.dinov3.parameters()).dtype
vae_dev = next(pipe.vae_encoder.parameters()).device
vae_dt = next(pipe.vae_encoder.parameters()).dtype
def teacher_feats(t):
with torch.no_grad():
t1 = F.layer_norm(pipe.dinov3(
pixel_values=T._DINOV3_NORMALIZE(t).to(dino_dev, dino_dt)
).float(), (1280,))
t2 = pipe.vae_encoder.encode(
(t * 2 - 1).to(vae_dev, vae_dt), deterministic=True)
return t1, t2
with torch.no_grad():
sf1 = dino_s(x)
sf2 = vae_s(x * 2 - 1)
# teacher features on the SAME canvas: isolates encoder error from crop
tf1, tf2 = teacher_feats(x)
row["cos1"] = round(float(F.cosine_similarity(sf1, tf1, dim=-1).mean()), 4)
row["cos2"] = round(float(F.cosine_similarity(sf2, tf2, dim=-1).mean()), 4)
# and once more on the teacher's own prepared image: encoder alone, no
# matte and no crop of ours in the way. The gap between the two pairs is
# what our matte costs, which cos1 above can't separate.
pc = prepared.convert("RGB")
if pc.size != (k3.CANVAS, k3.CANVAS):
pc = pc.resize((k3.CANVAS, k3.CANVAS), Image.LANCZOS)
xp = transforms.ToTensor()(pc).unsqueeze(0).to(dev)
with torch.no_grad():
pf1 = dino_s(xp)
pf2 = vae_s(xp * 2 - 1)
qf1, qf2 = teacher_feats(xp)
row["cos_enc1"] = round(float(F.cosine_similarity(pf1, qf1, dim=-1).mean()), 4)
row["cos_enc2"] = round(float(F.cosine_similarity(pf2, qf2, dim=-1).mean()), 4)
p(stem, "cos", row["cos1"], row["cos2"], "| encoder only",
row["cos_enc1"], row["cos_enc2"], "| mask",
row["mask"]["coverage"], "box", row["mask"]["box"])
cond_s = {"feature1": sf1, "feature2": pad2(sf2)}
for tag, which, steps, gs, shift in variants:
cond = cond_t if which == "t" else cond_s
t1 = time.time()
gen = torch.Generator(device=dev).manual_seed(42)
lat = pipe.sample_latent(cond, steps=steps, guidance_scale=gs,
shift=shift, generator=gen)
g = pipe.decode_latent(lat["latent"], num_gaussians=N_GAUSS)
ply = OUT / f"{stem}_{tag}.ply"
g.save_ply(str(ply))
row["runs"][tag] = {
**stats(g), "ply": ply.name, "steps": steps,
"g": gs, "shift": shift, "cond": which,
"mb": round(ply.stat().st_size / 1e6, 2),
"secs": round(time.time() - t1, 1),
}
p(f" {tag:6s} s{steps} g{gs} sh{shift} {row['runs'][tag]['secs']:6.1f}s "
f"op {row['runs'][tag]['opacity']} sc {row['runs'][tag]['scale']}")
row["secs"] = round(time.time() - t_img, 1)
except Exception as exc: # noqa: BLE001
import traceback # noqa: PLC0415
row["error"] = traceback.format_exc()[-2000:]
p(stem, "FAILED", type(exc).__name__, str(exc)[:200])
report.append(row)
(OUT / "report.json").write_text(json.dumps(report, indent=2))
tgz = OUT / "sheet.tgz"
with tarfile.open(tgz, "w:gz") as tf:
for f in sorted(OUT.iterdir()):
if f.name != tgz.name:
tf.add(f, arcname=f.name)
p("wrote", tgz, round(tgz.stat().st_size / 1e6, 1), "MB")
if __name__ == "__main__":
main()
|