File size: 6,600 Bytes
9c1761b
b0f1186
9c1761b
 
 
 
b0f1186
 
 
 
9c1761b
b0f1186
9c1761b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0f1186
9c1761b
b0f1186
9c1761b
 
 
 
 
 
 
b0f1186
 
 
 
 
 
 
 
9c1761b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0f1186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c1761b
b0f1186
 
 
9c1761b
 
 
 
 
 
 
 
 
b0f1186
9c1761b
b0f1186
 
 
9c1761b
 
 
 
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
#!/usr/bin/env python3
"""OracleZoom - faithful extreme super-resolution of ONE image (4x -> 16x -> 64x -> 256x).

Self-contained: this repo + auto-downloaded Stable Diffusion 3-medium and Qwen2.5-VL-3B.
Vendored Chain-of-Zoom code lives in ./coz, checkpoints in ./ckpt, merged model = merged_transformer.safetensors.

Usage (one image in, all scales out):
    python inference.py --input photo.jpg --output ./outputs
Writes: outputs/<name>_1x.png (input crop), _4x.png, _16x.png, _64x.png, _256x.png
(To batch many images, just call zoom_image() in a loop.)
"""
import argparse, os, sys, tempfile
import torch
from PIL import Image
from torchvision import transforms

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "coz"))   # vendored Chain-of-Zoom modules

COZ_PROMPT = ("The second image is a zoom-in of the first image. Based on this knowledge, "
              "what is in the second image? Give me a set of words.")
_to_tensor = transforms.Compose([transforms.ToTensor()])


def resize_and_center_crop(img, size):
    w, h = img.size
    scale = size / min(w, h)
    nw, nh = int(w * scale), int(h * scale)
    img = img.resize((nw, nh), Image.LANCZOS)
    l, t = (nw - size) // 2, (nh - size) // 2
    return img.crop((l, t, l + size, t + size))


class _SRArgs:
    def __init__(self, ckpt, sd3, process_size):
        self.lora_path = f"{ckpt}/SR_LoRA/model_20001.pkl"
        self.vae_path = f"{ckpt}/SR_VAE/vae_encoder_20001.pt"
        self.pretrained_model_name_or_path = sd3
        self.process_size = process_size
        self.lora_rank = 4
        self.merge_and_unload_lora = False
        self.mixed_precision = "fp16"


def build_sr(ckpt, sd3, merged, process_size):
    from osediff_sd3 import OSEDiff_SD3_TEST, SD3Euler
    from safetensors.torch import load_file
    sr = SD3Euler()
    for m in [sr.text_enc_1, sr.text_enc_2, sr.text_enc_3, sr.transformer, sr.vae]:
        m.to("cuda:0")
    sr.transformer.to("cuda:0", dtype=torch.float32)
    sr.vae.to("cuda:0", dtype=torch.float32)
    for m in [sr.text_enc_1, sr.text_enc_2, sr.text_enc_3, sr.transformer, sr.vae]:
        m.requires_grad_(False)
    sr_test = OSEDiff_SD3_TEST(_SRArgs(ckpt, sd3, process_size), sr)
    # load the merged OracleZoom transformer (replaces all transformer weights)
    sd = load_file(merged)
    dev = next(sr_test.model.transformer.parameters()).device
    sd = {k: v.to(dev, dtype=torch.float32) for k, v in sd.items()}
    miss, unexp = sr_test.model.transformer.load_state_dict(sd, strict=False)
    print(f"[OracleZoom] merged transformer loaded (missing={len(miss)} unexpected={len(unexp)})", flush=True)
    return sr_test


def build_vlm(ckpt):
    from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
    from qwen_vl_utils import process_vision_info
    from peft import PeftModel
    name = "Qwen/Qwen2.5-VL-3B-Instruct"
    model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
        name, torch_dtype="auto", device_map="auto", attn_implementation="sdpa")
    proc = AutoProcessor.from_pretrained(name)
    model = PeftModel.from_pretrained(model, f"{ckpt}/VLM_LoRA/checkpoint-10000").merge_and_unload().eval()
    return model, proc, process_vision_info


def vlm_prompt(model, proc, pvi, first, second, max_new_tokens=32):
    messages = [{"role": "system", "content": COZ_PROMPT},
                {"role": "user", "content": [{"type": "image", "image": first},
                                             {"type": "image", "image": second}]}]
    text = proc.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    ii, vi = pvi(messages)
    inputs = proc(text=[text], images=ii, videos=vi, padding=True, return_tensors="pt").to("cuda")
    gen = model.generate(**inputs, max_new_tokens=max_new_tokens)
    trimmed = [o[len(i):] for i, o in zip(inputs.input_ids, gen)]
    return proc.batch_decode(trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]


def zoom_image(sr, model, proc, pvi, image_path, out_dir,
               rec_num=4, upscale=4, process_size=512, max_new_tokens=32):
    """Super-resolve ONE image through the recursion; save every scale to out_dir. Returns list of paths."""
    os.makedirs(out_dir, exist_ok=True)
    stem = os.path.splitext(os.path.basename(image_path))[0]
    work = tempfile.mkdtemp()
    resize_and_center_crop(Image.open(image_path).convert("RGB"), process_size).save(f"{work}/0.png")
    for rec in range(rec_num):
        prev = Image.open(f"{work}/{rec}.png").convert("RGB")
        w, h = prev.size
        nw, nh = w // upscale, h // upscale
        crop = prev.crop(((w - nw) // 2, (h - nh) // 2, (w + nw) // 2, (h + nh) // 2))
        zoom = crop.resize((w, h), Image.BICUBIC)
        zoom.save(f"{work}/{rec + 1}_input.png")
        prompt = vlm_prompt(model, proc, pvi, f"{work}/{rec}.png", f"{work}/{rec + 1}_input.png", max_new_tokens)
        lq = _to_tensor(zoom).unsqueeze(0).to("cuda") * 2 - 1
        with torch.no_grad():
            out = torch.clamp(sr(lq, prompt=prompt)[0].cpu(), -1.0, 1.0)
        transforms.ToPILImage()(out * 0.5 + 0.5).save(f"{work}/{rec + 1}.png")
        print(f"  scale{rec + 1} ({4 ** (rec + 1)}x): {prompt}", flush=True)
    saved = []
    for s in range(rec_num + 1):
        dst = os.path.join(out_dir, f"{stem}_{4 ** s}x.png")
        Image.open(f"{work}/{s}.png").save(dst)
        saved.append(dst)
    return saved


def main():
    ap = argparse.ArgumentParser(description="OracleZoom: super-resolve one image to 4x/16x/64x/256x.")
    ap.add_argument("--input", required=True, help="path to ONE input image")
    ap.add_argument("--output", default="./outputs", help="output folder (all scales saved here)")
    ap.add_argument("--merged", default=os.path.join(HERE, "merged_transformer.safetensors"))
    ap.add_argument("--ckpt", default=os.path.join(HERE, "ckpt"))
    ap.add_argument("--sd3", default="stabilityai/stable-diffusion-3-medium-diffusers")
    ap.add_argument("--rec_num", type=int, default=4)
    ap.add_argument("--upscale", type=int, default=4)
    ap.add_argument("--process_size", type=int, default=512)
    ap.add_argument("--max_new_tokens", type=int, default=32)
    a = ap.parse_args()

    sr = build_sr(a.ckpt, a.sd3, a.merged, a.process_size)
    model, proc, pvi = build_vlm(a.ckpt)
    saved = zoom_image(sr, model, proc, pvi, a.input, a.output,
                       a.rec_num, a.upscale, a.process_size, a.max_new_tokens)
    print("[OracleZoom] saved:", *saved, sep="\n  ", flush=True)


if __name__ == "__main__":
    main()