Upload encoder/code/k3_encoder_distill.py with huggingface_hub
Browse files- encoder/code/k3_encoder_distill.py +1063 -0
encoder/code/k3_encoder_distill.py
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""K3 - distil the front end: photo -> (feature1, feature2), plus the matte.
|
| 2 |
+
|
| 3 |
+
The DiT is done and the decoder is done. What is still missing is the half that
|
| 4 |
+
turns a photo into the two conditioning tensors the DiT was trained on:
|
| 5 |
+
|
| 6 |
+
feature1 [1,4101,1280] DINOv3 ViT-H/16+ output, layer-normed over 1280
|
| 7 |
+
feature2 [1,4096, 128] Flux2-VAE encoder mean, BatchNorm-standardised
|
| 8 |
+
|
| 9 |
+
and the alpha matte that decides what the photo even is. All three teachers are
|
| 10 |
+
too big for a browser (DINO 840 M, VAE 30 M of pure convolution, BiRefNet 220 M
|
| 11 |
+
with deformable convs), so each gets a student:
|
| 12 |
+
|
| 13 |
+
matte small U-Net ~2 M image -> alpha (any H,W)
|
| 14 |
+
dino ConvNeXt-style token encoder ~19 M 1024^2 -> 4101x1280
|
| 15 |
+
vae small strided conv encoder ~5 M 1024^2 -> 4096x128
|
| 16 |
+
|
| 17 |
+
Why a conv student for DINO instead of a small ViT: the contract is 4101 tokens,
|
| 18 |
+
and a ViT at that length materialises a 4101x4101 score matrix per head unless
|
| 19 |
+
the attention is fused. A ConvNeXt block has no such matrix, exports to plain
|
| 20 |
+
Conv/LayerNorm/MatMul, and the ORT WebGPU conv path already measured 711 GFLOP/s
|
| 21 |
+
(PUBLISH.md section 2). The five prefix tokens (cls + 4 registers) are produced
|
| 22 |
+
from global average pooling, which is what those tokens mostly carry anyway.
|
| 23 |
+
|
| 24 |
+
Stages (FLIP_STAGES, default all):
|
| 25 |
+
|
| 26 |
+
setup clone upstream, download the four teacher checkpoints
|
| 27 |
+
prep run the teachers once, cache per-image targets + matte masks
|
| 28 |
+
train train the three students (resumable, checkpoints in OUT)
|
| 29 |
+
export ONNX + torch-vs-ORT parity, writes encoder_meta.json
|
| 30 |
+
e2e teacher DiT 2-step + decoder on student conditions vs teacher ones
|
| 31 |
+
|
| 32 |
+
Everything is sized for a single Colab/Kaggle T4. Defaults: 200 images, 40
|
| 33 |
+
epochs, batch 4 for the conv students, batch 1 for matte.
|
| 34 |
+
|
| 35 |
+
Env:
|
| 36 |
+
FLIP_WORK /content or /kaggle/working (auto)
|
| 37 |
+
FLIP_OUT output dir (default WORK/k3)
|
| 38 |
+
FLIP_STAGES comma list, default "setup,prep,train,export,e2e"
|
| 39 |
+
FLIP_N_IMG images to trace/train on (default 200)
|
| 40 |
+
FLIP_EPOCHS epochs per student (default 40)
|
| 41 |
+
FLIP_BATCH batch size for dino/vae (default 4)
|
| 42 |
+
FLIP_IMG_HF HF image dataset (default DownFlow/fuliji)
|
| 43 |
+
FLIP_IMAGE_DIR extra local images
|
| 44 |
+
FLIP_DINO_ARCH conv (default) or vit
|
| 45 |
+
FLIP_DINO_DIM student width (default 512)
|
| 46 |
+
FLIP_DINO_DEPTH blocks (default 8)
|
| 47 |
+
FLIP_FP16 export fp16 instead of fp32 (default 0)
|
| 48 |
+
FLIP_UPLOAD 1 = push encoder/ to idefinity/flip-web
|
| 49 |
+
HF_TOKEN upload token (or Kaggle secret / Colab userdata)
|
| 50 |
+
|
| 51 |
+
Run:
|
| 52 |
+
python3 k3_encoder_distill.py # all stages
|
| 53 |
+
FLIP_STAGES=prep python3 k3_encoder_distill.py
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
import argparse
|
| 57 |
+
import json
|
| 58 |
+
import math
|
| 59 |
+
import os
|
| 60 |
+
import shutil
|
| 61 |
+
import subprocess
|
| 62 |
+
import sys
|
| 63 |
+
import time
|
| 64 |
+
import traceback
|
| 65 |
+
import urllib.parse
|
| 66 |
+
import urllib.request
|
| 67 |
+
from pathlib import Path
|
| 68 |
+
|
| 69 |
+
WORK = Path(os.environ.get("FLIP_WORK")
|
| 70 |
+
or ("/content" if Path("/content").exists() else "/kaggle/working"))
|
| 71 |
+
REPO = WORK / "TripoSplat"
|
| 72 |
+
CKPT = REPO / "ckpts"
|
| 73 |
+
OUT = Path(os.environ.get("FLIP_OUT", str(WORK / "k3")))
|
| 74 |
+
CACHE = OUT / "cache"
|
| 75 |
+
WEIGHTS = OUT / "weights"
|
| 76 |
+
ONNX = OUT / "onnx"
|
| 77 |
+
|
| 78 |
+
SHA = "d8db9e0"
|
| 79 |
+
UPSTREAM = "https://github.com/VAST-AI-Research/TripoSplat.git"
|
| 80 |
+
TEACHER_REPO = "VAST-AI/TripoSplat"
|
| 81 |
+
PUBLISH_REPO = "idefinity/flip-student-2step"
|
| 82 |
+
|
| 83 |
+
N_IMG = int(os.environ.get("FLIP_N_IMG", "200"))
|
| 84 |
+
EPOCHS = int(os.environ.get("FLIP_EPOCHS", "40"))
|
| 85 |
+
BATCH = int(os.environ.get("FLIP_BATCH", "4"))
|
| 86 |
+
LR = float(os.environ.get("FLIP_LR", "3e-4"))
|
| 87 |
+
FP16 = os.environ.get("FLIP_FP16", "0") == "1"
|
| 88 |
+
UPLOAD = os.environ.get("FLIP_UPLOAD", "0") == "1"
|
| 89 |
+
IMG_HF = os.environ.get("FLIP_IMG_HF", "DownFlow/fuliji")
|
| 90 |
+
IMG_DIR = os.environ.get("FLIP_IMAGE_DIR", "")
|
| 91 |
+
STAGES = [s.strip() for s in os.environ.get(
|
| 92 |
+
"FLIP_STAGES", "setup,prep,train,export,e2e").split(",") if s.strip()]
|
| 93 |
+
|
| 94 |
+
CANVAS = 1024
|
| 95 |
+
DINO_OUT_TOKENS = 4101
|
| 96 |
+
DINO_DIM_OUT = 1280
|
| 97 |
+
VAE_TOKENS = 4096
|
| 98 |
+
VAE_DIM = 128
|
| 99 |
+
MATTE_SIZE = CANVAS
|
| 100 |
+
|
| 101 |
+
LOG = []
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def p(*a):
|
| 105 |
+
s = " ".join(str(x) for x in a)
|
| 106 |
+
LOG.append(s)
|
| 107 |
+
print(s, flush=True)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def note(msg):
|
| 111 |
+
p(msg)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def run(cmd, timeout=1800, env=None, check=False):
|
| 115 |
+
p("$", " ".join(str(c) for c in cmd)[:160])
|
| 116 |
+
r = subprocess.run([str(c) for c in cmd], capture_output=True, text=True,
|
| 117 |
+
timeout=timeout, env=env)
|
| 118 |
+
if r.returncode != 0:
|
| 119 |
+
p(" ", (r.stderr or r.stdout or "")[-500:].replace("\n", " "))
|
| 120 |
+
if check:
|
| 121 |
+
raise RuntimeError(f"command failed: {cmd}")
|
| 122 |
+
return r
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def ensure_dirs():
|
| 126 |
+
for d in (OUT, CACHE, WEIGHTS, ONNX):
|
| 127 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def hf_token():
|
| 131 |
+
tok = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
| 132 |
+
if tok:
|
| 133 |
+
return tok
|
| 134 |
+
try:
|
| 135 |
+
from kaggle_secrets import UserSecretsClient # noqa: PLC0415
|
| 136 |
+
return UserSecretsClient().get_secret("HF_TOKEN")
|
| 137 |
+
except Exception: # noqa: BLE001
|
| 138 |
+
return None
|
| 139 |
+
try:
|
| 140 |
+
from google.colab import userdata # noqa: PLC0415
|
| 141 |
+
return userdata.get("HF_TOKEN")
|
| 142 |
+
except Exception: # noqa: BLE001
|
| 143 |
+
return None
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# ---------------------------------------------------------------------------
|
| 147 |
+
# setup
|
| 148 |
+
# ---------------------------------------------------------------------------
|
| 149 |
+
|
| 150 |
+
def deps():
|
| 151 |
+
pkgs = ["huggingface_hub", "safetensors", "torchvision", "tqdm",
|
| 152 |
+
"onnx", "onnxruntime", "Pillow"]
|
| 153 |
+
p("installing", " ".join(pkgs))
|
| 154 |
+
run([sys.executable, "-m", "pip", "install", "-q", "--no-cache-dir", *pkgs],
|
| 155 |
+
timeout=1800)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def setup():
|
| 159 |
+
deps()
|
| 160 |
+
if not (REPO / ".git").exists():
|
| 161 |
+
run(["git", "clone", "-q", UPSTREAM, str(REPO)], timeout=900, check=True)
|
| 162 |
+
run(["git", "-C", str(REPO), "checkout", "-q", SHA], timeout=300)
|
| 163 |
+
from huggingface_hub import snapshot_download # noqa: PLC0415
|
| 164 |
+
if not (CKPT / ".complete").exists():
|
| 165 |
+
p("downloading teacher checkpoints (~3.5 GB)")
|
| 166 |
+
snapshot_download(repo_id=TEACHER_REPO, local_dir=str(CKPT),
|
| 167 |
+
token=hf_token())
|
| 168 |
+
(CKPT / ".complete").touch()
|
| 169 |
+
total = sum(f.stat().st_size for f in CKPT.rglob("*") if f.is_file())
|
| 170 |
+
p("ckpts", round(total / 1e9, 2), "GB")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# ---------------------------------------------------------------------------
|
| 174 |
+
# inputs
|
| 175 |
+
# ---------------------------------------------------------------------------
|
| 176 |
+
|
| 177 |
+
IMG_EXT = (".png", ".jpg", ".jpeg", ".webp")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def fetch_hf(n):
|
| 181 |
+
if not IMG_HF:
|
| 182 |
+
return []
|
| 183 |
+
dst = WORK / "inputs"
|
| 184 |
+
dst.mkdir(parents=True, exist_ok=True)
|
| 185 |
+
column = os.environ.get("FLIP_IMG_HF_COLUMN", "image").strip()
|
| 186 |
+
split = os.environ.get("FLIP_IMG_HF_SPLIT", "train").strip()
|
| 187 |
+
|
| 188 |
+
def get(url, timeout=90):
|
| 189 |
+
req = urllib.request.Request(url, headers={"User-Agent": "flip-k3"})
|
| 190 |
+
with urllib.request.urlopen(req, timeout=timeout) as fh:
|
| 191 |
+
return fh.read()
|
| 192 |
+
|
| 193 |
+
cfg = None
|
| 194 |
+
try:
|
| 195 |
+
q = urllib.parse.urlencode({"dataset": IMG_HF})
|
| 196 |
+
splits = json.loads(get(
|
| 197 |
+
f"https://datasets-server.huggingface.co/splits?{q}"))["splits"]
|
| 198 |
+
if splits:
|
| 199 |
+
cfg, split = splits[0].get("config"), splits[0].get("split", split)
|
| 200 |
+
except Exception as exc: # noqa: BLE001
|
| 201 |
+
p(" splits lookup failed", type(exc).__name__)
|
| 202 |
+
|
| 203 |
+
got, offset = [], 0
|
| 204 |
+
while len(got) < n:
|
| 205 |
+
q = {"dataset": IMG_HF, "split": split, "offset": offset,
|
| 206 |
+
"length": min(100, n - len(got))}
|
| 207 |
+
if cfg:
|
| 208 |
+
q["config"] = cfg
|
| 209 |
+
try:
|
| 210 |
+
js = json.loads(get("https://datasets-server.huggingface.co/rows?"
|
| 211 |
+
+ urllib.parse.urlencode(q)))
|
| 212 |
+
except Exception as exc: # noqa: BLE001
|
| 213 |
+
p(" rows failed", type(exc).__name__)
|
| 214 |
+
break
|
| 215 |
+
rows = js.get("rows") or []
|
| 216 |
+
if not rows:
|
| 217 |
+
break
|
| 218 |
+
for r in rows:
|
| 219 |
+
cell = (r.get("row") or {}).get(column)
|
| 220 |
+
src = cell.get("src") if isinstance(cell, dict) else None
|
| 221 |
+
if not src:
|
| 222 |
+
continue
|
| 223 |
+
path = dst / f"hf_{len(got):04d}.jpg"
|
| 224 |
+
if path.exists() and path.stat().st_size > 1024:
|
| 225 |
+
got.append(path)
|
| 226 |
+
continue
|
| 227 |
+
try:
|
| 228 |
+
blob = get(src, timeout=60)
|
| 229 |
+
if blob[:2] != b"\xff\xd8" and blob[:8] != b"\x89PNG\r\n\x1a\n":
|
| 230 |
+
continue
|
| 231 |
+
tmp = path.with_suffix(".jpg.part")
|
| 232 |
+
tmp.write_bytes(blob)
|
| 233 |
+
os.replace(tmp, path)
|
| 234 |
+
got.append(path)
|
| 235 |
+
except Exception: # noqa: BLE001
|
| 236 |
+
continue
|
| 237 |
+
if len(got) >= n:
|
| 238 |
+
break
|
| 239 |
+
offset += len(rows)
|
| 240 |
+
if offset >= (js.get("num_rows_total") or 0):
|
| 241 |
+
break
|
| 242 |
+
p("fetched", len(got), "images from", IMG_HF)
|
| 243 |
+
return got
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def pick_images():
|
| 247 |
+
imgs = []
|
| 248 |
+
if IMG_DIR:
|
| 249 |
+
root = Path(IMG_DIR)
|
| 250 |
+
if root.is_dir():
|
| 251 |
+
imgs += sorted(q for q in root.rglob("*")
|
| 252 |
+
if q.suffix.lower() in IMG_EXT)
|
| 253 |
+
elif root.is_file():
|
| 254 |
+
imgs.append(root)
|
| 255 |
+
p("local images", len(imgs))
|
| 256 |
+
if len(imgs) < N_IMG:
|
| 257 |
+
imgs += fetch_hf(N_IMG - len(imgs))
|
| 258 |
+
if len(imgs) < N_IMG:
|
| 259 |
+
ex = REPO / "static" / "example_inputs"
|
| 260 |
+
if ex.is_dir():
|
| 261 |
+
imgs += sorted(q for q in ex.rglob("*")
|
| 262 |
+
if q.suffix.lower() in IMG_EXT)
|
| 263 |
+
if not imgs:
|
| 264 |
+
raise RuntimeError("no input images: set FLIP_IMG_HF or FLIP_IMAGE_DIR")
|
| 265 |
+
p("inputs", len(imgs), "->", imgs[0], "...", imgs[-1])
|
| 266 |
+
return imgs[:N_IMG]
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
# ---------------------------------------------------------------------------
|
| 270 |
+
# teacher + preprocessing
|
| 271 |
+
# ---------------------------------------------------------------------------
|
| 272 |
+
|
| 273 |
+
def load_teacher():
|
| 274 |
+
import torch # noqa: F401
|
| 275 |
+
sys.path.insert(0, str(REPO))
|
| 276 |
+
import triposplat as T # noqa: PLC0415
|
| 277 |
+
dev = "cuda" if __import__("torch").cuda.is_available() else "cpu"
|
| 278 |
+
dino = T.load_dinov3(str(CKPT / "clip_vision/dino_v3_vit_h.safetensors"),
|
| 279 |
+
device=dev, dtype=__import__("torch").bfloat16)
|
| 280 |
+
vae = T.load_vae_encoder(str(CKPT / "vae/flux2-vae.safetensors"),
|
| 281 |
+
device=dev, dtype=__import__("torch").bfloat16)
|
| 282 |
+
rmbg = T.load_rmbg(str(CKPT / "background_removal/birefnet.safetensors"),
|
| 283 |
+
device=dev, dtype=__import__("torch").float16)
|
| 284 |
+
return T, dino, vae, rmbg, dev
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def resize_min_side(image, target=CANVAS):
|
| 288 |
+
w, h = image.size
|
| 289 |
+
s = target / min(w, h)
|
| 290 |
+
w2 = max(32, int(round(w * s / 32)) * 32)
|
| 291 |
+
h2 = max(32, int(round(h * s / 32)) * 32)
|
| 292 |
+
return image.resize((w2, h2), __import__("PIL.Image", fromlist=["Image"]).LANCZOS)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def erode_alpha(alpha, radius=1):
|
| 296 |
+
"""MinFilter(2r+1) on the alpha channel, the same shrink upstream applies."""
|
| 297 |
+
from PIL import Image, ImageFilter # noqa: PLC0415
|
| 298 |
+
im = Image.fromarray(alpha, mode="L")
|
| 299 |
+
return im.filter(ImageFilter.MinFilter(2 * radius + 1))
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def square_crop_box(alpha, expand=1.2):
|
| 303 |
+
import numpy as np # noqa: PLC0415
|
| 304 |
+
a = np.asarray(alpha)
|
| 305 |
+
ys, xs = np.nonzero(a)
|
| 306 |
+
if len(xs) == 0:
|
| 307 |
+
h, w = a.shape
|
| 308 |
+
side = min(h, w)
|
| 309 |
+
return (w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2
|
| 310 |
+
x0, x1, y0, y1 = xs.min(), xs.max(), ys.min(), ys.max()
|
| 311 |
+
cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
|
| 312 |
+
half = max(x1 - x0, y1 - y0) / 2 * expand
|
| 313 |
+
return (int(round(cx - half)), int(round(cy - half)),
|
| 314 |
+
int(round(cx + half)), int(round(cy + half)))
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def make_sample(T, dino, vae, rmbg, path, idx):
|
| 318 |
+
import numpy as np # noqa: PLC0415
|
| 319 |
+
import torch # noqa: PLC0415
|
| 320 |
+
import torch.nn.functional as F # noqa: PLC0415
|
| 321 |
+
from PIL import Image # noqa: PLC0415
|
| 322 |
+
from torchvision import transforms # noqa: PLC0415
|
| 323 |
+
|
| 324 |
+
stem = f"{idx:04d}"
|
| 325 |
+
files = {k: CACHE / f"{k}_{stem}.npy" for k in ("f1", "f2", "canvas")}
|
| 326 |
+
matte = {k: CACHE / f"{k}_{stem}.png" for k in ("matte_rgb", "matte_alpha")}
|
| 327 |
+
if all(f.exists() for f in files.values()) and all(f.exists() for f in matte.values()):
|
| 328 |
+
return True
|
| 329 |
+
|
| 330 |
+
image = Image.open(path).convert("RGB")
|
| 331 |
+
rgb = resize_min_side(image)
|
| 332 |
+
with torch.no_grad():
|
| 333 |
+
rgba = rmbg.remove_background(rgb)
|
| 334 |
+
alpha_raw = np.array(rgba.getchannel(3), dtype=np.uint8)
|
| 335 |
+
alpha_er = np.array(erode_alpha(alpha_raw, 1), dtype=np.uint8)
|
| 336 |
+
|
| 337 |
+
box = square_crop_box(alpha_er)
|
| 338 |
+
cropped = rgba.crop(box).resize((CANVAS, CANVAS), Image.LANCZOS)
|
| 339 |
+
canvas = Image.new("RGB", (CANVAS, CANVAS), (0, 0, 0))
|
| 340 |
+
canvas.paste(cropped, mask=cropped.split()[3])
|
| 341 |
+
|
| 342 |
+
dino_dtype = next(dino.parameters()).dtype
|
| 343 |
+
vae_dtype = next(vae.parameters()).dtype
|
| 344 |
+
dino_device = next(dino.parameters()).device
|
| 345 |
+
vae_device = next(vae.parameters()).device
|
| 346 |
+
x = transforms.ToTensor()(canvas).unsqueeze(0)
|
| 347 |
+
with torch.no_grad():
|
| 348 |
+
f1 = dino(pixel_values=T._DINOV3_NORMALIZE(x).to(dino_device, dino_dtype))
|
| 349 |
+
f1 = F.layer_norm(f1.float(), f1.shape[-1:])
|
| 350 |
+
f2 = vae.encode(x.to(vae_device, vae_dtype) * 2 - 1, deterministic=True)
|
| 351 |
+
|
| 352 |
+
np.save(files["f1"], f1[0].half().cpu().numpy())
|
| 353 |
+
np.save(files["f2"], f2[0].half().cpu().numpy())
|
| 354 |
+
np.save(files["canvas"], (np.asarray(canvas, dtype=np.uint8).transpose(2, 0, 1)))
|
| 355 |
+
Image.fromarray(np.asarray(rgb, dtype=np.uint8)).save(matte["matte_rgb"])
|
| 356 |
+
Image.fromarray(alpha_raw).save(matte["matte_alpha"])
|
| 357 |
+
return True
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def prep():
|
| 361 |
+
imgs = pick_images()
|
| 362 |
+
T, dino, vae, rmbg, dev = load_teacher()
|
| 363 |
+
p("teacher on", dev)
|
| 364 |
+
t0 = time.time()
|
| 365 |
+
ok = 0
|
| 366 |
+
for i, path in enumerate(imgs):
|
| 367 |
+
try:
|
| 368 |
+
if make_sample(T, dino, vae, rmbg, path, i):
|
| 369 |
+
ok += 1
|
| 370 |
+
except Exception: # noqa: BLE001
|
| 371 |
+
p(" skip", path.name, traceback.format_exc().splitlines()[-1][:160])
|
| 372 |
+
if (i + 1) % 10 == 0:
|
| 373 |
+
dt = time.time() - t0
|
| 374 |
+
p(f" prep {i + 1}/{len(imgs)} {dt / (i + 1):.2f} s/img")
|
| 375 |
+
p("prep done", ok, "of", len(imgs))
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# ---------------------------------------------------------------------------
|
| 379 |
+
# students
|
| 380 |
+
# ---------------------------------------------------------------------------
|
| 381 |
+
|
| 382 |
+
def build_dino():
|
| 383 |
+
import torch
|
| 384 |
+
import torch.nn as nn
|
| 385 |
+
|
| 386 |
+
class LN2d(nn.Module):
|
| 387 |
+
def __init__(self, dim, eps=1e-6):
|
| 388 |
+
super().__init__()
|
| 389 |
+
self.ln = nn.LayerNorm(dim, eps=eps)
|
| 390 |
+
|
| 391 |
+
def forward(self, x):
|
| 392 |
+
return self.ln(x.permute(0, 2, 3, 1).contiguous()).permute(0, 3, 1, 2)
|
| 393 |
+
|
| 394 |
+
class ConvBlock(nn.Module):
|
| 395 |
+
"""ConvNeXt block: depthwise 7x7 then an inverted bottleneck."""
|
| 396 |
+
|
| 397 |
+
def __init__(self, dim, mlp_ratio=4):
|
| 398 |
+
super().__init__()
|
| 399 |
+
hidden = int(dim * mlp_ratio)
|
| 400 |
+
self.norm = LN2d(dim)
|
| 401 |
+
self.dw = nn.Conv2d(dim, dim, 7, padding=3, groups=dim)
|
| 402 |
+
self.pw1 = nn.Conv2d(dim, hidden, 1)
|
| 403 |
+
self.act = nn.GELU()
|
| 404 |
+
self.pw2 = nn.Conv2d(hidden, dim, 1)
|
| 405 |
+
self.gamma = nn.Parameter(torch.ones(dim) * 1e-6)
|
| 406 |
+
|
| 407 |
+
def forward(self, x):
|
| 408 |
+
h = self.norm(x)
|
| 409 |
+
h = self.dw(h)
|
| 410 |
+
h = self.pw1(h)
|
| 411 |
+
h = self.act(h)
|
| 412 |
+
h = self.pw2(h)
|
| 413 |
+
return x + self.gamma.view(1, -1, 1, 1) * h
|
| 414 |
+
|
| 415 |
+
class DinoConvStudent(nn.Module):
|
| 416 |
+
def __init__(self, dim=512, depth=8, patch=16, out_dim=DINO_DIM_OUT):
|
| 417 |
+
super().__init__()
|
| 418 |
+
self.stem = nn.Conv2d(3, dim, patch, patch)
|
| 419 |
+
self.stem_norm = LN2d(dim)
|
| 420 |
+
self.blocks = nn.Sequential(*[ConvBlock(dim) for _ in range(depth)])
|
| 421 |
+
self.norm = nn.LayerNorm(dim)
|
| 422 |
+
self.prefix = nn.Linear(dim, 5 * dim)
|
| 423 |
+
self.proj = nn.Linear(dim, out_dim)
|
| 424 |
+
self.out_norm = nn.LayerNorm(out_dim, elementwise_affine=False)
|
| 425 |
+
|
| 426 |
+
def forward(self, x):
|
| 427 |
+
h = self.stem_norm(self.stem(x))
|
| 428 |
+
h = self.blocks(h)
|
| 429 |
+
b, c, hh, ww = h.shape
|
| 430 |
+
tok = h.flatten(2).transpose(1, 2)
|
| 431 |
+
tok = self.norm(tok)
|
| 432 |
+
g = tok.mean(dim=1)
|
| 433 |
+
pre = self.prefix(g).reshape(b, 5, c)
|
| 434 |
+
tok = torch.cat([pre, tok], dim=1)
|
| 435 |
+
return self.out_norm(self.proj(tok))
|
| 436 |
+
|
| 437 |
+
class VitBlock(nn.Module):
|
| 438 |
+
def __init__(self, dim, heads=8, mlp_ratio=4):
|
| 439 |
+
super().__init__()
|
| 440 |
+
self.n1 = nn.LayerNorm(dim)
|
| 441 |
+
self.qkv = nn.Linear(dim, 3 * dim)
|
| 442 |
+
self.proj = nn.Linear(dim, dim)
|
| 443 |
+
self.n2 = nn.LayerNorm(dim)
|
| 444 |
+
hidden = dim * mlp_ratio
|
| 445 |
+
self.fc1 = nn.Linear(dim, hidden)
|
| 446 |
+
self.fc2 = nn.Linear(hidden, dim)
|
| 447 |
+
self.heads = heads
|
| 448 |
+
|
| 449 |
+
def forward(self, x):
|
| 450 |
+
b, n, c = x.shape
|
| 451 |
+
qkv = self.qkv(self.n1(x)).reshape(b, n, 3, self.heads, c // self.heads)
|
| 452 |
+
q, k, v = qkv.permute(2, 0, 3, 1, 4)
|
| 453 |
+
att = (q @ k.transpose(-2, -1)) * (c // self.heads) ** -0.5
|
| 454 |
+
att = att.softmax(dim=-1)
|
| 455 |
+
o = (att @ v).transpose(1, 2).reshape(b, n, c)
|
| 456 |
+
x = x + self.proj(o)
|
| 457 |
+
x = x + self.fc2(torch.nn.functional.gelu(self.fc1(self.n2(x))))
|
| 458 |
+
return x
|
| 459 |
+
|
| 460 |
+
class DinoVitStudent(nn.Module):
|
| 461 |
+
def __init__(self, dim=384, depth=8, heads=6, patch=16,
|
| 462 |
+
out_dim=DINO_DIM_OUT, tokens=4096):
|
| 463 |
+
super().__init__()
|
| 464 |
+
self.stem = nn.Conv2d(3, dim, patch, patch)
|
| 465 |
+
self.cls = nn.Parameter(torch.zeros(1, 1, dim))
|
| 466 |
+
self.reg = nn.Parameter(torch.zeros(1, 4, dim))
|
| 467 |
+
self.pos = nn.Parameter(torch.zeros(1, tokens, dim))
|
| 468 |
+
self.blocks = nn.ModuleList([VitBlock(dim, heads) for _ in range(depth)])
|
| 469 |
+
self.norm = nn.LayerNorm(dim)
|
| 470 |
+
self.proj = nn.Linear(dim, out_dim)
|
| 471 |
+
self.out_norm = nn.LayerNorm(out_dim, elementwise_affine=False)
|
| 472 |
+
|
| 473 |
+
def forward(self, x):
|
| 474 |
+
h = self.stem(x).flatten(2).transpose(1, 2) + self.pos
|
| 475 |
+
h = torch.cat([self.cls.expand(h.shape[0], -1, -1),
|
| 476 |
+
self.reg.expand(h.shape[0], -1, -1), h], dim=1)
|
| 477 |
+
for blk in self.blocks:
|
| 478 |
+
h = blk(h)
|
| 479 |
+
return self.out_norm(self.proj(self.norm(h)))
|
| 480 |
+
|
| 481 |
+
arch = os.environ.get("FLIP_DINO_ARCH", "conv")
|
| 482 |
+
dim = int(os.environ.get("FLIP_DINO_DIM", "512"))
|
| 483 |
+
depth = int(os.environ.get("FLIP_DINO_DEPTH", "8"))
|
| 484 |
+
if arch == "vit":
|
| 485 |
+
return DinoVitStudent(dim=min(dim, 512), depth=depth)
|
| 486 |
+
return DinoConvStudent(dim=dim, depth=depth)
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def build_vae():
|
| 490 |
+
import torch.nn as nn
|
| 491 |
+
|
| 492 |
+
class Res(nn.Module):
|
| 493 |
+
def __init__(self, cin, cout):
|
| 494 |
+
super().__init__()
|
| 495 |
+
self.n1 = nn.GroupNorm(8, cin)
|
| 496 |
+
self.c1 = nn.Conv2d(cin, cout, 3, 1, 1)
|
| 497 |
+
self.n2 = nn.GroupNorm(8, cout)
|
| 498 |
+
self.c2 = nn.Conv2d(cout, cout, 3, 1, 1)
|
| 499 |
+
self.act = nn.SiLU()
|
| 500 |
+
self.skip = nn.Conv2d(cin, cout, 1) if cin != cout else nn.Identity()
|
| 501 |
+
|
| 502 |
+
def forward(self, x):
|
| 503 |
+
h = self.c1(self.act(self.n1(x)))
|
| 504 |
+
h = self.c2(self.act(self.n2(h)))
|
| 505 |
+
return h + self.skip(x)
|
| 506 |
+
|
| 507 |
+
class VaeStudent(nn.Module):
|
| 508 |
+
def __init__(self, ch=(64, 128, 256, 256, 256), out_dim=VAE_DIM):
|
| 509 |
+
super().__init__()
|
| 510 |
+
self.inp = nn.Conv2d(3, ch[0], 3, 1, 1)
|
| 511 |
+
self.enc = nn.ModuleList()
|
| 512 |
+
self.down = nn.ModuleList()
|
| 513 |
+
prev = ch[0]
|
| 514 |
+
for c in ch[1:]:
|
| 515 |
+
self.enc.append(Res(prev, c))
|
| 516 |
+
self.down.append(nn.Conv2d(c, c, 3, 2, 1))
|
| 517 |
+
prev = c
|
| 518 |
+
self.mid = nn.Sequential(Res(prev, prev), Res(prev, prev))
|
| 519 |
+
self.out = nn.Conv2d(prev, out_dim, 1)
|
| 520 |
+
|
| 521 |
+
def forward(self, x):
|
| 522 |
+
h = self.inp(x)
|
| 523 |
+
for res, down in zip(self.enc, self.down):
|
| 524 |
+
h = down(res(h))
|
| 525 |
+
h = self.mid(h)
|
| 526 |
+
h = self.out(h)
|
| 527 |
+
return h.flatten(2).transpose(1, 2)
|
| 528 |
+
|
| 529 |
+
return VaeStudent()
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def build_matte():
|
| 533 |
+
import torch
|
| 534 |
+
import torch.nn as nn
|
| 535 |
+
import torch.nn.functional as F
|
| 536 |
+
|
| 537 |
+
class Block(nn.Module):
|
| 538 |
+
def __init__(self, cin, cout):
|
| 539 |
+
super().__init__()
|
| 540 |
+
self.c1 = nn.Conv2d(cin, cout, 3, 1, 1)
|
| 541 |
+
self.c2 = nn.Conv2d(cout, cout, 3, 1, 1)
|
| 542 |
+
self.n1 = nn.GroupNorm(8, cout)
|
| 543 |
+
self.n2 = nn.GroupNorm(8, cout)
|
| 544 |
+
self.act = nn.SiLU()
|
| 545 |
+
|
| 546 |
+
def forward(self, x):
|
| 547 |
+
x = self.act(self.n1(self.c1(x)))
|
| 548 |
+
return self.act(self.n2(self.c2(x)))
|
| 549 |
+
|
| 550 |
+
class MatteStudent(nn.Module):
|
| 551 |
+
def __init__(self, base=24):
|
| 552 |
+
super().__init__()
|
| 553 |
+
c = [base, base * 2, base * 4, base * 8, base * 8]
|
| 554 |
+
self.e0 = Block(3, c[0])
|
| 555 |
+
self.e1 = Block(c[0], c[1])
|
| 556 |
+
self.e2 = Block(c[1], c[2])
|
| 557 |
+
self.e3 = Block(c[2], c[3])
|
| 558 |
+
self.e4 = Block(c[3], c[4])
|
| 559 |
+
self.pool = nn.MaxPool2d(2)
|
| 560 |
+
self.d3 = Block(c[4] + c[3], c[3])
|
| 561 |
+
self.d2 = Block(c[3] + c[2], c[2])
|
| 562 |
+
self.d1 = Block(c[2] + c[1], c[1])
|
| 563 |
+
self.d0 = Block(c[1] + c[0], c[0])
|
| 564 |
+
self.head = nn.Conv2d(c[0], 1, 1)
|
| 565 |
+
|
| 566 |
+
def up(self, x):
|
| 567 |
+
return F.interpolate(x, scale_factor=2, mode="bilinear",
|
| 568 |
+
align_corners=False)
|
| 569 |
+
|
| 570 |
+
def forward(self, x):
|
| 571 |
+
e0 = self.e0(x)
|
| 572 |
+
e1 = self.e1(self.pool(e0))
|
| 573 |
+
e2 = self.e2(self.pool(e1))
|
| 574 |
+
e3 = self.e3(self.pool(e2))
|
| 575 |
+
e4 = self.e4(self.pool(e3))
|
| 576 |
+
d3 = self.d3(torch.cat([self.up(e4), e3], 1))
|
| 577 |
+
d2 = self.d2(torch.cat([self.up(d3), e2], 1))
|
| 578 |
+
d1 = self.d1(torch.cat([self.up(d2), e1], 1))
|
| 579 |
+
d0 = self.d0(torch.cat([self.up(d1), e0], 1))
|
| 580 |
+
return torch.sigmoid(self.head(d0))
|
| 581 |
+
|
| 582 |
+
return MatteStudent()
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def params(model):
|
| 586 |
+
return sum(q.numel() for q in model.parameters()) / 1e6
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
# ---------------------------------------------------------------------------
|
| 590 |
+
# datasets / training
|
| 591 |
+
# ---------------------------------------------------------------------------
|
| 592 |
+
|
| 593 |
+
def cache_items():
|
| 594 |
+
items = sorted(CACHE.glob("f1_*.npy"))
|
| 595 |
+
return [q.name[3:-4] for q in items]
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def load_canvas(stem):
|
| 599 |
+
import numpy as np # noqa: PLC0415
|
| 600 |
+
a = np.load(CACHE / f"canvas_{stem}.npy")
|
| 601 |
+
return a
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def train_dino(device):
|
| 605 |
+
import numpy as np # noqa: PLC0415
|
| 606 |
+
import torch # noqa: PLC0415
|
| 607 |
+
import torch.nn.functional as F # noqa: PLC0415
|
| 608 |
+
|
| 609 |
+
stems = cache_items()
|
| 610 |
+
if not stems:
|
| 611 |
+
raise RuntimeError("no cached targets; run prep first")
|
| 612 |
+
model = build_dino().to(device)
|
| 613 |
+
p(f"dino student {params(model):.1f} M params")
|
| 614 |
+
opt = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=0.05)
|
| 615 |
+
steps = max(1, (len(stems) // BATCH) * EPOCHS)
|
| 616 |
+
sched = torch.optim.lr_scheduler.OneCycleLR(
|
| 617 |
+
opt, max_lr=LR, total_steps=steps, pct_start=0.1)
|
| 618 |
+
scaler = torch.amp.GradScaler("cuda", enabled=device == "cuda")
|
| 619 |
+
ckpt = WEIGHTS / "dino.pt"
|
| 620 |
+
start = 0
|
| 621 |
+
if ckpt.exists():
|
| 622 |
+
st = torch.load(ckpt, map_location=device)
|
| 623 |
+
model.load_state_dict(st["model"])
|
| 624 |
+
opt.load_state_dict(st["opt"])
|
| 625 |
+
start = st["epoch"]
|
| 626 |
+
for _ in range(start * max(1, len(stems) // BATCH)):
|
| 627 |
+
if sched.last_epoch < steps - 1:
|
| 628 |
+
sched.step()
|
| 629 |
+
p("resume dino at epoch", start)
|
| 630 |
+
|
| 631 |
+
for ep in range(start, EPOCHS):
|
| 632 |
+
t0 = time.time()
|
| 633 |
+
order = np.random.RandomState(ep).permutation(len(stems))
|
| 634 |
+
acc = n = 0
|
| 635 |
+
for it in range(0, len(order) - BATCH + 1, BATCH):
|
| 636 |
+
idx = order[it:it + BATCH]
|
| 637 |
+
x = torch.stack([torch.from_numpy(load_canvas(stems[j])).float() / 255.0
|
| 638 |
+
for j in idx]).to(device)
|
| 639 |
+
tgt = torch.stack([
|
| 640 |
+
torch.from_numpy(np.load(CACHE / f"f1_{stems[j]}.npy")).float()
|
| 641 |
+
for j in idx]).to(device)
|
| 642 |
+
with torch.autocast("cuda", torch.float16, enabled=device == "cuda"):
|
| 643 |
+
pred = model(x)
|
| 644 |
+
cos = F.cosine_similarity(pred.float(), tgt, dim=-1).mean()
|
| 645 |
+
loss = (1 - cos) + 0.1 * F.mse_loss(pred.float(), tgt)
|
| 646 |
+
opt.zero_grad(set_to_none=True)
|
| 647 |
+
scaler.scale(loss).backward()
|
| 648 |
+
scaler.unscale_(opt)
|
| 649 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 650 |
+
scaler.step(opt)
|
| 651 |
+
scaler.update()
|
| 652 |
+
if sched.last_epoch < steps - 1:
|
| 653 |
+
sched.step()
|
| 654 |
+
acc += float(cos.detach())
|
| 655 |
+
n += 1
|
| 656 |
+
if n % 20 == 0:
|
| 657 |
+
p(f" dino ep{ep} {n} cos {acc / n:.4f} "
|
| 658 |
+
f"lr {sched.get_last_lr()[0]:.2e}")
|
| 659 |
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(),
|
| 660 |
+
"scaler": scaler.state_dict(),
|
| 661 |
+
"epoch": ep + 1, "cos": acc / max(n, 1)},
|
| 662 |
+
ckpt)
|
| 663 |
+
p(f"dino epoch {ep + 1}/{EPOCHS} cos {acc / max(n, 1):.4f} "
|
| 664 |
+
f"({time.time() - t0:.0f}s)")
|
| 665 |
+
return model
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def train_vae(device):
|
| 669 |
+
import numpy as np # noqa: PLC0415
|
| 670 |
+
import torch # noqa: PLC0415
|
| 671 |
+
import torch.nn.functional as F # noqa: PLC0415
|
| 672 |
+
|
| 673 |
+
stems = cache_items()
|
| 674 |
+
model = build_vae().to(device)
|
| 675 |
+
p(f"vae student {params(model):.1f} M params")
|
| 676 |
+
opt = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=0.05)
|
| 677 |
+
steps = max(1, (len(stems) // BATCH) * EPOCHS)
|
| 678 |
+
sched = torch.optim.lr_scheduler.OneCycleLR(
|
| 679 |
+
opt, max_lr=LR, total_steps=steps, pct_start=0.1)
|
| 680 |
+
scaler = torch.amp.GradScaler("cuda", enabled=device == "cuda")
|
| 681 |
+
ckpt = WEIGHTS / "vae.pt"
|
| 682 |
+
start = 0
|
| 683 |
+
if ckpt.exists():
|
| 684 |
+
st = torch.load(ckpt, map_location=device)
|
| 685 |
+
model.load_state_dict(st["model"])
|
| 686 |
+
opt.load_state_dict(st["opt"])
|
| 687 |
+
start = st["epoch"]
|
| 688 |
+
for _ in range(start * max(1, len(stems) // BATCH)):
|
| 689 |
+
if sched.last_epoch < steps - 1:
|
| 690 |
+
sched.step()
|
| 691 |
+
p("resume vae at epoch", start)
|
| 692 |
+
|
| 693 |
+
for ep in range(start, EPOCHS):
|
| 694 |
+
t0 = time.time()
|
| 695 |
+
order = np.random.RandomState(1000 + ep).permutation(len(stems))
|
| 696 |
+
acc = n = 0
|
| 697 |
+
for it in range(0, len(order) - BATCH + 1, BATCH):
|
| 698 |
+
idx = order[it:it + BATCH]
|
| 699 |
+
x = torch.stack([torch.from_numpy(load_canvas(stems[j])).float() / 255.0
|
| 700 |
+
for j in idx]).to(device)
|
| 701 |
+
x = x * 2 - 1
|
| 702 |
+
tgt = torch.stack([
|
| 703 |
+
torch.from_numpy(np.load(CACHE / f"f2_{stems[j]}.npy")).float()
|
| 704 |
+
for j in idx]).to(device)
|
| 705 |
+
with torch.autocast("cuda", torch.float16, enabled=device == "cuda"):
|
| 706 |
+
pred = model(x)
|
| 707 |
+
cos = F.cosine_similarity(pred.float(), tgt, dim=-1).mean()
|
| 708 |
+
loss = (1 - cos) + 0.5 * F.mse_loss(pred.float(), tgt)
|
| 709 |
+
opt.zero_grad(set_to_none=True)
|
| 710 |
+
scaler.scale(loss).backward()
|
| 711 |
+
scaler.unscale_(opt)
|
| 712 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 713 |
+
scaler.step(opt)
|
| 714 |
+
scaler.update()
|
| 715 |
+
if sched.last_epoch < steps - 1:
|
| 716 |
+
sched.step()
|
| 717 |
+
acc += float(cos.detach())
|
| 718 |
+
n += 1
|
| 719 |
+
if n % 20 == 0:
|
| 720 |
+
p(f" vae ep{ep} {n} cos {acc / n:.4f} "
|
| 721 |
+
f"lr {sched.get_last_lr()[0]:.2e}")
|
| 722 |
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(),
|
| 723 |
+
"scaler": scaler.state_dict(),
|
| 724 |
+
"epoch": ep + 1, "cos": acc / max(n, 1)}, ckpt)
|
| 725 |
+
p(f"vae epoch {ep + 1}/{EPOCHS} cos {acc / max(n, 1):.4f} "
|
| 726 |
+
f"({time.time() - t0:.0f}s)")
|
| 727 |
+
return model
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
def train_matte(device):
|
| 731 |
+
import numpy as np # noqa: PLC0415
|
| 732 |
+
import torch # noqa: PLC0415
|
| 733 |
+
import torch.nn.functional as F # noqa: PLC0415
|
| 734 |
+
from PIL import Image # noqa: PLC0415
|
| 735 |
+
|
| 736 |
+
stems = cache_items()
|
| 737 |
+
model = build_matte().to(device)
|
| 738 |
+
p(f"matte student {params(model):.1f} M params")
|
| 739 |
+
opt = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=0.01)
|
| 740 |
+
steps = max(1, len(stems) * EPOCHS)
|
| 741 |
+
sched = torch.optim.lr_scheduler.OneCycleLR(
|
| 742 |
+
opt, max_lr=LR, total_steps=steps, pct_start=0.1)
|
| 743 |
+
scaler = torch.amp.GradScaler("cuda", enabled=device == "cuda")
|
| 744 |
+
ckpt = WEIGHTS / "matte.pt"
|
| 745 |
+
start = 0
|
| 746 |
+
if ckpt.exists():
|
| 747 |
+
st = torch.load(ckpt, map_location=device)
|
| 748 |
+
model.load_state_dict(st["model"])
|
| 749 |
+
opt.load_state_dict(st["opt"])
|
| 750 |
+
start = st["epoch"]
|
| 751 |
+
for _ in range(start * len(stems)):
|
| 752 |
+
if sched.last_epoch < steps - 1:
|
| 753 |
+
sched.step()
|
| 754 |
+
p("resume matte at epoch", start)
|
| 755 |
+
|
| 756 |
+
for ep in range(start, EPOCHS):
|
| 757 |
+
t0 = time.time()
|
| 758 |
+
order = np.random.RandomState(2000 + ep).permutation(len(stems))
|
| 759 |
+
acc = n = 0
|
| 760 |
+
for j in order:
|
| 761 |
+
rgb = Image.open(CACHE / f"matte_rgb_{stems[j]}.png").convert("RGB")
|
| 762 |
+
alpha = Image.open(CACHE / f"matte_alpha_{stems[j]}.png").convert("L")
|
| 763 |
+
x = torch.from_numpy(np.asarray(rgb).astype(np.float32) / 255.0)
|
| 764 |
+
y = torch.from_numpy(np.asarray(alpha).astype(np.float32) / 255.0)
|
| 765 |
+
x = x.permute(2, 0, 1).unsqueeze(0).to(device)
|
| 766 |
+
y = y.unsqueeze(0).unsqueeze(0).to(device)
|
| 767 |
+
with torch.autocast("cuda", torch.float16, enabled=device == "cuda"):
|
| 768 |
+
pred = model(x).float()
|
| 769 |
+
bce = F.binary_cross_entropy(pred, y)
|
| 770 |
+
inter = (pred * y).sum()
|
| 771 |
+
dice = 1 - (2 * inter + 1) / (pred.sum() + y.sum() + 1)
|
| 772 |
+
loss = bce + dice
|
| 773 |
+
opt.zero_grad(set_to_none=True)
|
| 774 |
+
scaler.scale(loss).backward()
|
| 775 |
+
scaler.unscale_(opt)
|
| 776 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 777 |
+
scaler.step(opt)
|
| 778 |
+
scaler.update()
|
| 779 |
+
if sched.last_epoch < steps - 1:
|
| 780 |
+
sched.step()
|
| 781 |
+
acc += 1 - float(bce.detach())
|
| 782 |
+
n += 1
|
| 783 |
+
if n % 40 == 0:
|
| 784 |
+
p(f" matte ep{ep} {n}/{len(stems)} bce {float(bce):.4f} "
|
| 785 |
+
f"dice {float(dice):.4f}")
|
| 786 |
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(),
|
| 787 |
+
"scaler": scaler.state_dict(),
|
| 788 |
+
"epoch": ep + 1, "bce": 1 - acc / max(n, 1)}, ckpt)
|
| 789 |
+
p(f"matte epoch {ep + 1}/{EPOCHS} "
|
| 790 |
+
f"acc {acc / max(n, 1):.4f} ({time.time() - t0:.0f}s)")
|
| 791 |
+
return model
|
| 792 |
+
|
| 793 |
+
|
| 794 |
+
def train():
|
| 795 |
+
import torch # noqa: PLC0415
|
| 796 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 797 |
+
p("train on", device)
|
| 798 |
+
train_dino(device)
|
| 799 |
+
train_vae(device)
|
| 800 |
+
train_matte(device)
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
# ---------------------------------------------------------------------------
|
| 804 |
+
# export
|
| 805 |
+
# ---------------------------------------------------------------------------
|
| 806 |
+
|
| 807 |
+
def load_weights(model, name, device):
|
| 808 |
+
import torch # noqa: PLC0415
|
| 809 |
+
ckpt = WEIGHTS / f"{name}.pt"
|
| 810 |
+
if not ckpt.exists():
|
| 811 |
+
raise RuntimeError(f"missing {ckpt}")
|
| 812 |
+
st = torch.load(ckpt, map_location=device)
|
| 813 |
+
model.load_state_dict(st["model"])
|
| 814 |
+
return model.to(device).eval()
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def export_model(model, example, path, inputs, outputs, dynamic=None, fp16=False):
|
| 818 |
+
import torch # noqa: PLC0415
|
| 819 |
+
model = model.eval()
|
| 820 |
+
if fp16:
|
| 821 |
+
model = model.half()
|
| 822 |
+
example = example.half()
|
| 823 |
+
torch.onnx.export(
|
| 824 |
+
model, example, str(path),
|
| 825 |
+
input_names=inputs, output_names=outputs,
|
| 826 |
+
opset_version=17, do_constant_folding=True,
|
| 827 |
+
dynamic_axes=dynamic, dynamo=False,
|
| 828 |
+
)
|
| 829 |
+
return path
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
def parity(path, inputs, outputs, torch_out, tol=0.999):
|
| 833 |
+
import numpy as np # noqa: PLC0415
|
| 834 |
+
import onnxruntime as ort # noqa: PLC0415
|
| 835 |
+
providers = ["CPUExecutionProvider"]
|
| 836 |
+
try:
|
| 837 |
+
if ort.get_device() == "GPU":
|
| 838 |
+
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
|
| 839 |
+
except Exception: # noqa: BLE001
|
| 840 |
+
pass
|
| 841 |
+
sess = ort.InferenceSession(str(path), providers=providers)
|
| 842 |
+
feed = {k: v.detach().cpu().numpy() for k, v in inputs.items()}
|
| 843 |
+
got = sess.run(None, feed)
|
| 844 |
+
worst = 1.0
|
| 845 |
+
for a, b in zip(got, torch_out):
|
| 846 |
+
a = a.astype(np.float32).reshape(-1)
|
| 847 |
+
b = b.detach().cpu().float().numpy().reshape(-1)
|
| 848 |
+
denom = np.linalg.norm(a) * np.linalg.norm(b) + 1e-12
|
| 849 |
+
cos = float(np.dot(a, b) / denom)
|
| 850 |
+
worst = min(worst, cos)
|
| 851 |
+
return worst, [tuple(g.shape) for g in got]
|
| 852 |
+
|
| 853 |
+
|
| 854 |
+
def export():
|
| 855 |
+
import torch # noqa: PLC0415
|
| 856 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 857 |
+
meta = {"files": {}, "shapes": {}, "params": {}}
|
| 858 |
+
|
| 859 |
+
dino = load_weights(build_dino(), "dino", device)
|
| 860 |
+
ex = torch.rand(1, 3, CANVAS, CANVAS, device=device)
|
| 861 |
+
with torch.no_grad():
|
| 862 |
+
ref = dino(ex)
|
| 863 |
+
path = ONNX / "dino_student.onnx"
|
| 864 |
+
export_model(dino.float(), ex, path, ["image"], ["feature1"], fp16=False)
|
| 865 |
+
cos, shapes = parity(path, {"image": ex}, ["feature1"], [ref])
|
| 866 |
+
p(f"dino onnx {path.stat().st_size / 1e6:.1f} MB parity cos {cos:.6f}")
|
| 867 |
+
meta["files"]["dino"] = path.name
|
| 868 |
+
meta["shapes"]["dino"] = {"input": [1, 3, CANVAS, CANVAS],
|
| 869 |
+
"output": list(shapes[0])}
|
| 870 |
+
meta["params"]["dino"] = round(params(dino), 2)
|
| 871 |
+
|
| 872 |
+
vae = load_weights(build_vae(), "vae", device)
|
| 873 |
+
ex = torch.rand(1, 3, CANVAS, CANVAS, device=device) * 2 - 1
|
| 874 |
+
with torch.no_grad():
|
| 875 |
+
ref = vae(ex)
|
| 876 |
+
path = ONNX / "vae_student.onnx"
|
| 877 |
+
export_model(vae.float(), ex, path, ["image"], ["feature2"], fp16=False)
|
| 878 |
+
cos, shapes = parity(path, {"image": ex}, ["feature2"], [ref])
|
| 879 |
+
p(f"vae onnx {path.stat().st_size / 1e6:.1f} MB parity cos {cos:.6f}")
|
| 880 |
+
meta["files"]["vae"] = path.name
|
| 881 |
+
meta["shapes"]["vae"] = {"input": [1, 3, CANVAS, CANVAS],
|
| 882 |
+
"output": list(shapes[0])}
|
| 883 |
+
meta["params"]["vae"] = round(params(vae), 2)
|
| 884 |
+
|
| 885 |
+
matte = load_weights(build_matte(), "matte", device)
|
| 886 |
+
ex = torch.rand(1, 3, 512, 768, device=device)
|
| 887 |
+
with torch.no_grad():
|
| 888 |
+
ref = matte(ex)
|
| 889 |
+
path = ONNX / "matte_student.onnx"
|
| 890 |
+
export_model(matte.float(), ex, path, ["image"], ["alpha"],
|
| 891 |
+
dynamic={"image": {2: "h", 3: "w"},
|
| 892 |
+
"alpha": {2: "h", 3: "w"}})
|
| 893 |
+
cos, shapes = parity(path, {"image": ex}, ["alpha"], [ref])
|
| 894 |
+
p(f"matte onnx {path.stat().st_size / 1e6:.1f} MB parity cos {cos:.6f}")
|
| 895 |
+
meta["files"]["matte"] = path.name
|
| 896 |
+
meta["shapes"]["matte"] = {"input": [1, 3, "h", "w"], "output": [1, 1, "h", "w"]}
|
| 897 |
+
meta["params"]["matte"] = round(params(matte), 2)
|
| 898 |
+
|
| 899 |
+
meta.update({
|
| 900 |
+
"preprocess": {
|
| 901 |
+
"resize": "shortest side 1024, dims rounded to /32",
|
| 902 |
+
"matte_input": "rgb / 255",
|
| 903 |
+
"dino_input": "canvas rgb / 255",
|
| 904 |
+
"vae_input": "canvas rgb / 255 * 2 - 1",
|
| 905 |
+
"canonical": CANVAS,
|
| 906 |
+
},
|
| 907 |
+
"tokens": {"dino": DINO_OUT_TOKENS, "vae": VAE_TOKENS},
|
| 908 |
+
})
|
| 909 |
+
(OUT / "encoder_meta.json").write_text(json.dumps(meta, indent=2))
|
| 910 |
+
p("wrote", OUT / "encoder_meta.json")
|
| 911 |
+
return meta
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
def upload():
|
| 915 |
+
if not UPLOAD:
|
| 916 |
+
return
|
| 917 |
+
from huggingface_hub import HfApi # noqa: PLC0415
|
| 918 |
+
tok = hf_token()
|
| 919 |
+
if not tok:
|
| 920 |
+
raise RuntimeError("FLIP_UPLOAD=1 but no HF token")
|
| 921 |
+
api = HfApi(token=tok)
|
| 922 |
+
api.create_repo(PUBLISH_REPO, private=False, exist_ok=True)
|
| 923 |
+
for f in sorted(ONNX.glob("*.onnx")) + [OUT / "encoder_meta.json"]:
|
| 924 |
+
api.upload_file(path_or_fileobj=str(f), path_in_repo=f.name,
|
| 925 |
+
repo_id=PUBLISH_REPO)
|
| 926 |
+
p("uploaded", f.name)
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
# ---------------------------------------------------------------------------
|
| 930 |
+
# end to end
|
| 931 |
+
# ---------------------------------------------------------------------------
|
| 932 |
+
|
| 933 |
+
def build_teacher_pipeline(T, device):
|
| 934 |
+
files = {
|
| 935 |
+
"ckpt": CKPT / "diffusion_models/triposplat_fp16.safetensors",
|
| 936 |
+
"dec": CKPT / "vae/triposplat_vae_decoder_fp16.safetensors",
|
| 937 |
+
}
|
| 938 |
+
return T.TripoSplatPipeline(
|
| 939 |
+
ckpt_path=str(files["ckpt"]), decoder_path=str(files["dec"]),
|
| 940 |
+
dinov3_path=str(CKPT / "clip_vision/dino_v3_vit_h.safetensors"),
|
| 941 |
+
flux2_vae_encoder_path=str(CKPT / "vae/flux2-vae.safetensors"),
|
| 942 |
+
rmbg_path=str(CKPT / "background_removal/birefnet.safetensors"),
|
| 943 |
+
device=device)
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
def ply_stats(gauss):
|
| 947 |
+
import numpy as np # noqa: PLC0415
|
| 948 |
+
op = gauss.get_opacity.detach().float().cpu().numpy().reshape(-1)
|
| 949 |
+
sc = gauss.get_scaling.detach().float().cpu().numpy().reshape(-1)
|
| 950 |
+
xyz = gauss.get_xyz.detach().float().cpu().numpy().reshape(-1, 3)
|
| 951 |
+
return {"opacity": float(op.mean()), "scale": float(sc.mean()),
|
| 952 |
+
"x": [float(xyz[:, 0].min()), float(xyz[:, 0].max())]}
|
| 953 |
+
|
| 954 |
+
|
| 955 |
+
def e2e():
|
| 956 |
+
"""Teacher DiT + decoder on student conditions vs teacher conditions.
|
| 957 |
+
|
| 958 |
+
This is the acceptance test that matters: a feature cosine of 0.98 means
|
| 959 |
+
nothing if the contact sheet moves. We run the teacher's own sampler for
|
| 960 |
+
two steps (the student's grid) and compare the decoded Gaussian statistics.
|
| 961 |
+
"""
|
| 962 |
+
import numpy as np # noqa: PLC0415
|
| 963 |
+
import torch # noqa: PLC0415
|
| 964 |
+
from PIL import Image # noqa: PLC0415
|
| 965 |
+
import torch.nn.functional as F # noqa: PLC0415
|
| 966 |
+
from torchvision import transforms # noqa: PLC0415
|
| 967 |
+
|
| 968 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 969 |
+
sys.path.insert(0, str(REPO))
|
| 970 |
+
import triposplat as T # noqa: PLC0415
|
| 971 |
+
pipe = build_teacher_pipeline(T, device)
|
| 972 |
+
dino, vae = pipe.dinov3, pipe.vae_encoder
|
| 973 |
+
dino_s = load_weights(build_dino(), "dino", device)
|
| 974 |
+
vae_s = load_weights(build_vae(), "vae", device)
|
| 975 |
+
|
| 976 |
+
stems = cache_items()[:4]
|
| 977 |
+
rows = []
|
| 978 |
+
for stem in stems:
|
| 979 |
+
canvas = Image.fromarray(
|
| 980 |
+
np.load(CACHE / f"canvas_{stem}.npy").transpose(1, 2, 0))
|
| 981 |
+
x = transforms.ToTensor()(canvas).unsqueeze(0).to(device)
|
| 982 |
+
dino_dtype = next(dino.parameters()).dtype
|
| 983 |
+
vae_dtype = next(vae.parameters()).dtype
|
| 984 |
+
dino_device = next(dino.parameters()).device
|
| 985 |
+
vae_device = next(vae.parameters()).device
|
| 986 |
+
with torch.no_grad():
|
| 987 |
+
tf1 = F.layer_norm(dino(
|
| 988 |
+
pixel_values=T._DINOV3_NORMALIZE(x).to(dino_device, dino_dtype)
|
| 989 |
+
).float(), (1280,))
|
| 990 |
+
tf2 = vae.encode(x.to(vae_device, vae_dtype) * 2 - 1,
|
| 991 |
+
deterministic=True)
|
| 992 |
+
sf1 = dino_s(x)
|
| 993 |
+
sf2 = vae_s(x * 2 - 1)
|
| 994 |
+
cos1 = float(F.cosine_similarity(sf1, tf1, dim=-1).mean())
|
| 995 |
+
cos2 = float(F.cosine_similarity(sf2, tf2, dim=-1).mean())
|
| 996 |
+
|
| 997 |
+
def pad2(f):
|
| 998 |
+
z = torch.zeros(f.shape[0], 5, f.shape[2], dtype=f.dtype,
|
| 999 |
+
device=f.device)
|
| 1000 |
+
return torch.cat([z, f], dim=1)
|
| 1001 |
+
|
| 1002 |
+
tf2p, sf2p = pad2(tf2), pad2(sf2)
|
| 1003 |
+
out = {}
|
| 1004 |
+
lats = {}
|
| 1005 |
+
for tag, f1, f2 in (("teacher", tf1, tf2p),
|
| 1006 |
+
("dino", sf1, tf2p),
|
| 1007 |
+
("full", sf1, sf2p)):
|
| 1008 |
+
cond = {"feature1": f1, "feature2": f2}
|
| 1009 |
+
gen = torch.Generator(device=device).manual_seed(42)
|
| 1010 |
+
lat = pipe.sample_latent(cond, steps=2, guidance_scale=1.0,
|
| 1011 |
+
shift=1.0, generator=gen)
|
| 1012 |
+
lats[tag] = lat["latent"].detach().float()
|
| 1013 |
+
g = pipe.decode_latent(lat["latent"], num_gaussians=65536)
|
| 1014 |
+
out[tag] = ply_stats(g)
|
| 1015 |
+
lat_cos = float(F.cosine_similarity(
|
| 1016 |
+
lats["full"], lats["teacher"], dim=-1).mean())
|
| 1017 |
+
lat_cos_d = float(F.cosine_similarity(
|
| 1018 |
+
lats["dino"], lats["teacher"], dim=-1).mean())
|
| 1019 |
+
rows.append({"stem": stem, "cos1": cos1, "cos2": cos2,
|
| 1020 |
+
"lat_cos_full": lat_cos, "lat_cos_dino": lat_cos_d, **out})
|
| 1021 |
+
p(f"e2e {stem} cos1 {cos1:.4f} cos2 {cos2:.4f} "
|
| 1022 |
+
f"lat {lat_cos:.4f}/{lat_cos_d:.4f} "
|
| 1023 |
+
f"opacity teacher/dino/full "
|
| 1024 |
+
f"{out['teacher']['opacity']:.4f}/"
|
| 1025 |
+
f"{out['dino']['opacity']:.4f}/{out['full']['opacity']:.4f}")
|
| 1026 |
+
|
| 1027 |
+
(OUT / "e2e.json").write_text(json.dumps(rows, indent=2))
|
| 1028 |
+
p("wrote", OUT / "e2e.json")
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
+
# ---------------------------------------------------------------------------
|
| 1032 |
+
|
| 1033 |
+
def main():
|
| 1034 |
+
ap = argparse.ArgumentParser(description=__doc__)
|
| 1035 |
+
ap.add_argument("--stages", default=",".join(STAGES))
|
| 1036 |
+
args = ap.parse_args()
|
| 1037 |
+
ensure_dirs()
|
| 1038 |
+
stages = [s.strip() for s in args.stages.split(",") if s.strip()]
|
| 1039 |
+
p("stages", stages)
|
| 1040 |
+
try:
|
| 1041 |
+
if "setup" in stages:
|
| 1042 |
+
setup()
|
| 1043 |
+
if "prep" in stages:
|
| 1044 |
+
prep()
|
| 1045 |
+
if "train" in stages:
|
| 1046 |
+
train()
|
| 1047 |
+
if "export" in stages:
|
| 1048 |
+
export()
|
| 1049 |
+
if "upload" in stages:
|
| 1050 |
+
upload()
|
| 1051 |
+
if "e2e" in stages:
|
| 1052 |
+
e2e()
|
| 1053 |
+
except Exception: # noqa: BLE001
|
| 1054 |
+
p("FAILED")
|
| 1055 |
+
p(traceback.format_exc()[-4000:])
|
| 1056 |
+
raise
|
| 1057 |
+
finally:
|
| 1058 |
+
(OUT / "log.txt").write_text("\n".join(LOG))
|
| 1059 |
+
p("log ->", OUT / "log.txt")
|
| 1060 |
+
|
| 1061 |
+
|
| 1062 |
+
if __name__ == "__main__":
|
| 1063 |
+
main()
|