zziang commited on
Commit
b958326
·
verified ·
1 Parent(s): 87c398c

Upload encoder/code/k3_sheet.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. encoder/code/k3_sheet.py +197 -0
encoder/code/k3_sheet.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """K3 sheet - four renders per photo, so quality is looked at instead of argued about.
2
+
3
+ The acceptance test we never ran: put the teacher's 20-step result next to what our
4
+ chain produces for the same photo, and look. Numbers like "lat cos 0.84" are a proxy
5
+ for this image, and they were made on cosplay studio photos, not on the photos people
6
+ actually drop in.
7
+
8
+ Four PLYs per input, all through the teacher's own decoder so the only differences are
9
+ the ones we care about:
10
+
11
+ t20 teacher conditions, 20 steps, its own sampler (guidance 3, shift 3) <- truth
12
+ t2 teacher conditions, 2 steps, guidance 3 <- cost of fewer steps
13
+ s2 student conditions, 2 steps, guidance 3 <- cost of our encoder
14
+ s2g student conditions, 2 steps, guidance 1, shift 1 <- what the browser runs
15
+
16
+ Also written per image: the canvas the student encoder saw (1024^2, black), the alpha
17
+ the student matte produced, and the teacher's own prepared image. When a sheet cell is
18
+ fog, one of those three says which half broke.
19
+
20
+ Run it from the k3 driver (stage "sheet"), or directly:
21
+ FLIP_WORK=/kaggle/working python3 k3_sheet.py /kaggle/working/eval
22
+ """
23
+
24
+ import importlib.util
25
+ import json
26
+ import os
27
+ import pathlib
28
+ import sys
29
+ import tarfile
30
+ import time
31
+
32
+ WORK = pathlib.Path(os.environ.get("FLIP_WORK", "/kaggle/working"))
33
+ K3 = WORK / "k3_encoder_distill.py"
34
+ IMGDIR = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else str(WORK / "eval"))
35
+ OUT = WORK / "k3" / "sheet"
36
+ OUT.mkdir(parents=True, exist_ok=True)
37
+ N_GAUSS = 65536
38
+
39
+ os.environ.setdefault("FLIP_WORK", str(WORK))
40
+
41
+
42
+ def p(*a):
43
+ print(f"[{time.strftime('%H:%M:%S')}]", *a, flush=True)
44
+
45
+
46
+ def load_k3():
47
+ spec = importlib.util.spec_from_file_location("k3", str(K3))
48
+ mod = importlib.util.module_from_spec(spec)
49
+ spec.loader.exec_module(mod)
50
+ return mod
51
+
52
+
53
+ def pad2(f):
54
+ import torch # noqa: PLC0415
55
+ z = torch.zeros(f.shape[0], 5, f.shape[2], dtype=f.dtype, device=f.device)
56
+ return torch.cat([z, f], dim=1)
57
+
58
+
59
+ def stats(g):
60
+ return {
61
+ "n": int(g.get_xyz.shape[0]),
62
+ "opacity": round(float(g.get_opacity.detach().float().mean()), 4),
63
+ "scale": round(float(g.get_scaling.detach().float().mean()), 5),
64
+ "x": [round(float(g.get_xyz[:, 0].min()), 3),
65
+ round(float(g.get_xyz[:, 0].max()), 3)],
66
+ }
67
+
68
+
69
+ def main():
70
+ import numpy as np # noqa: PLC0415
71
+ import torch # noqa: PLC0415
72
+ import torch.nn.functional as F # noqa: PLC0415
73
+ from PIL import Image # noqa: PLC0415
74
+ from torchvision import transforms # noqa: PLC0415
75
+
76
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
77
+ p("device", dev, "torch", torch.__version__)
78
+ k3 = load_k3()
79
+ sys.path.insert(0, str(k3.REPO))
80
+ import triposplat as T # noqa: PLC0415
81
+
82
+ imgs = sorted(q for q in IMGDIR.rglob("*")
83
+ if q.suffix.lower() in (".png", ".jpg", ".jpeg", ".webp"))
84
+ limit = int(os.environ.get("FLIP_SHEET_N", "0"))
85
+ if limit:
86
+ imgs = imgs[:limit]
87
+ if not imgs:
88
+ raise SystemExit(f"no images under {IMGDIR}")
89
+ p("images", len(imgs), [q.name for q in imgs])
90
+
91
+ t0 = time.time()
92
+ pipe = k3.build_teacher_pipeline(T, dev)
93
+ p("teacher pipeline", round(time.time() - t0, 1), "s")
94
+ dino_s = k3.load_weights(k3.build_dino(), "dino", dev)
95
+ vae_s = k3.load_weights(k3.build_vae(), "vae", dev)
96
+ matte_s = k3.load_weights(k3.build_matte(), "matte", dev)
97
+ p("student encoders loaded")
98
+
99
+ report = []
100
+ for i, path in enumerate(imgs):
101
+ stem = path.stem
102
+ row = {"image": path.name, "stem": stem, "runs": {}}
103
+ try:
104
+ t_img = time.time()
105
+ image = Image.open(path).convert("RGB")
106
+ row["size"] = list(image.size)
107
+
108
+ # --- teacher path: its own preprocess, its own encoder
109
+ prepared = pipe.preprocess_image(image)
110
+ prepared.save(OUT / f"{stem}_prepared.png")
111
+ row["prepared"] = list(prepared.size)
112
+ gen = torch.Generator(device=dev).manual_seed(42)
113
+ cond_t = pipe.encode_image(prepared, generator=gen)
114
+
115
+ # --- student path: mirror of site/src/pipeline.js loadCondition
116
+ rgb = k3.resize_min_side(image)
117
+ w, h = rgb.size
118
+ xin = transforms.ToTensor()(rgb).unsqueeze(0).to(dev)
119
+ with torch.no_grad():
120
+ a = matte_s(xin)[0, 0].float().cpu().numpy()
121
+ alpha = (np.clip(a, 0, 1) * 255).astype(np.uint8)
122
+ Image.fromarray(alpha, "L").save(OUT / f"{stem}_mask.png")
123
+ er = np.array(k3.erode_alpha(alpha, 1), dtype=np.uint8)
124
+ box = k3.square_crop_box(er)
125
+ rgba = rgb.convert("RGBA")
126
+ rgba.putalpha(Image.fromarray(alpha, "L"))
127
+ cropped = rgba.crop(box).resize((k3.CANVAS, k3.CANVAS), Image.LANCZOS)
128
+ canvas = Image.new("RGB", (k3.CANVAS, k3.CANVAS), (0, 0, 0))
129
+ canvas.paste(cropped, mask=cropped.split()[3])
130
+ canvas.save(OUT / f"{stem}_canvas.png")
131
+ fx = alpha.astype(np.float32) / 255.0
132
+ row["mask"] = {
133
+ "resized": [w, h],
134
+ "box": [int(v) for v in box],
135
+ "fill": round(float(fx.mean()), 4),
136
+ "coverage": round(float((fx > 0.5).mean()), 4),
137
+ }
138
+
139
+ x = transforms.ToTensor()(canvas).unsqueeze(0).to(dev)
140
+ dino_dev = next(pipe.dinov3.parameters()).device
141
+ dino_dt = next(pipe.dinov3.parameters()).dtype
142
+ vae_dev = next(pipe.vae_encoder.parameters()).device
143
+ vae_dt = next(pipe.vae_encoder.parameters()).dtype
144
+ with torch.no_grad():
145
+ sf1 = dino_s(x)
146
+ sf2 = vae_s(x * 2 - 1)
147
+ # teacher features on the SAME canvas: isolates encoder error from crop
148
+ tf1 = F.layer_norm(pipe.dinov3(
149
+ pixel_values=T._DINOV3_NORMALIZE(x).to(dino_dev, dino_dt)
150
+ ).float(), (1280,))
151
+ tf2 = pipe.vae_encoder.encode(
152
+ (x * 2 - 1).to(vae_dev, vae_dt), deterministic=True)
153
+ row["cos1"] = round(float(F.cosine_similarity(sf1, tf1, dim=-1).mean()), 4)
154
+ row["cos2"] = round(float(F.cosine_similarity(sf2, tf2, dim=-1).mean()), 4)
155
+ p(stem, "cos", row["cos1"], row["cos2"], "mask",
156
+ row["mask"]["coverage"], "box", row["mask"]["box"])
157
+
158
+ cond_s = {"feature1": sf1, "feature2": pad2(sf2)}
159
+ variants = [
160
+ ("t20", cond_t, 20, 3.0, 3.0),
161
+ ("t2", cond_t, 2, 3.0, 3.0),
162
+ ("s2", cond_s, 2, 3.0, 3.0),
163
+ ("s2g", cond_s, 2, 1.0, 1.0),
164
+ ]
165
+ for tag, cond, steps, gs, shift in variants:
166
+ t1 = time.time()
167
+ gen = torch.Generator(device=dev).manual_seed(42)
168
+ lat = pipe.sample_latent(cond, steps=steps, guidance_scale=gs,
169
+ shift=shift, generator=gen)
170
+ g = pipe.decode_latent(lat["latent"], num_gaussians=N_GAUSS)
171
+ ply = OUT / f"{stem}_{tag}.ply"
172
+ g.save_ply(str(ply))
173
+ row["runs"][tag] = {
174
+ **stats(g), "ply": ply.name,
175
+ "mb": round(ply.stat().st_size / 1e6, 2),
176
+ "secs": round(time.time() - t1, 1),
177
+ }
178
+ p(f" {tag:4s} {row['runs'][tag]['secs']:6.1f}s "
179
+ f"op {row['runs'][tag]['opacity']} sc {row['runs'][tag]['scale']}")
180
+ row["secs"] = round(time.time() - t_img, 1)
181
+ except Exception as exc: # noqa: BLE001
182
+ import traceback # noqa: PLC0415
183
+ row["error"] = traceback.format_exc()[-2000:]
184
+ p(stem, "FAILED", type(exc).__name__, str(exc)[:200])
185
+ report.append(row)
186
+ (OUT / "report.json").write_text(json.dumps(report, indent=2))
187
+
188
+ tgz = OUT / "sheet.tgz"
189
+ with tarfile.open(tgz, "w:gz") as tf:
190
+ for f in sorted(OUT.iterdir()):
191
+ if f.name != tgz.name:
192
+ tf.add(f, arcname=f.name)
193
+ p("wrote", tgz, round(tgz.stat().st_size / 1e6, 1), "MB")
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()