File size: 4,156 Bytes
076b9c5
5ae84d8
076b9c5
 
 
 
5ae84d8
 
 
 
 
076b9c5
df2e4da
5ae84d8
 
 
 
 
 
 
 
076b9c5
5ae84d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
076b9c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df2e4da
 
076b9c5
 
 
 
 
df2e4da
 
 
 
 
 
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
"""FlashReFocus for the Hugging Face Space (ZeroGPU), on the flashrefocus package (a copy of the repository's).

ZeroGPU gives a GPU only inside @spaces.GPU calls: every model is built on the CPU, moved to `cuda` at import time as an
ordinary module (Stage I with two separate U-Nets, no weights swapped in place), and the per-session image and depth
stay on the CPU between calls. The weights are downloaded on start; the FlashReFocus ones come from a private model
repository read with the HF_TOKEN secret."""
import os
from pathlib import Path

import numpy as np
import torch
from huggingface_hub import hf_hub_download, snapshot_download
from PIL import Image

HERE = Path(__file__).resolve().parent
WEIGHT_REPO = os.environ.get("FLASHREFOCUS_WEIGHT_REPO", "ming0531/FlashReFocus-weights")


def fetch_weights():
    """Weights into ./weights (skipped when FLASHREFOCUS_WEIGHTS already points to a local copy)."""
    if "FLASHREFOCUS_WEIGHTS" in os.environ:
        return
    w = HERE / "weights"
    sd_files = ["model_index.json", "scheduler/scheduler_config.json", "tokenizer/*", "text_encoder/config.json",
                "text_encoder/model.safetensors", "unet/config.json", "unet/diffusion_pytorch_model.safetensors",
                "vae/config.json", "vae/diffusion_pytorch_model.safetensors"]
    snapshot_download("sd2-community/stable-diffusion-2-1-base", allow_patterns=sd_files,
                      local_dir=w / "stable-diffusion-2-1-base")
    snapshot_download("stable-diffusion-v1-5/stable-diffusion-v1-5",
                      allow_patterns=["vae/config.json", "vae/diffusion_pytorch_model.safetensors"],
                      local_dir=w / "stable-diffusion-v1-5")
    hf_hub_download("depth-anything/Depth-Anything-V2-Large", "depth_anything_v2_vitl.pth", local_dir=w)
    for f in ("stage1.pkl", "refiner.pt", "renderer.pt"):
        hf_hub_download(WEIGHT_REPO, f, local_dir=w, token=os.environ.get("HF_TOKEN"))
    os.environ["FLASHREFOCUS_WEIGHTS"] = str(w)


fetch_weights()
os.environ.setdefault("FLASHREFOCUS_BUILD_DEVICE", "cpu")      # third_party/PiSA-SR here builds on this device
from flashrefocus import Refocuser, Scene, paths  # noqa: E402
from flashrefocus.deblurring import Deblurrer  # noqa: E402
from flashrefocus.image_io import image_to_tensor, tensor_to_uint8  # noqa: E402
from flashrefocus.rendering import DepthEstimator, depth_edits, load_renderer  # noqa: E402


def build_refocuser():
    deblurrer = Deblurrer.from_pretrained(separate_unets=True, device="cpu")
    del deblurrer.model.text_encoder               # the empty-prompt embedding is computed once, at construction
    return Refocuser(deblurrer.to("cuda"), DepthEstimator.from_pretrained(device="cpu").to("cuda"),
                     load_renderer(paths.RENDERER, "cpu").to("cuda"))


refocuser = build_refocuser()


@torch.no_grad()
def restore(rgb, omega):
    """HxWx3 uint8 (sides multiples of 16) -> restored image (HxWx3 uint8) and its depth (HxW float32, 1 = far)."""
    refocuser.deblurrer.detail_weight = float(omega)
    scene = refocuser.prepare(image_to_tensor(rgb).cuda())
    return tensor_to_uint8(scene.image), scene.depth[0].cpu().numpy()


@torch.no_grad()
def render(rgb, depth, focal_depth, strength):
    """Bokeh (HxWx3 uint8) of the restored image with blur strength K and focal depth D_f; depth may be edited."""
    d = torch.from_numpy(np.ascontiguousarray(depth, dtype=np.float32))[None].cuda()
    return tensor_to_uint8(refocuser.render(Scene(image_to_tensor(rgb).cuda(), d), strength, focal_depth))


def two_focal_planes(depth, focal_depth_a, focal_depth_b):
    return depth_edits.two_focal_planes(torch.from_numpy(depth)[None], focal_depth_a, focal_depth_b)[0].numpy()


def tilt_shift(height, width, row, band=0.06):
    return depth_edits.tilt_shift(height, width, row, band=band)[0].numpy()


def depth_from_image(image, height, width):
    """A user depth map (near is bright, any size) -> D in [0, 1], 1 = far."""
    grey = Image.fromarray(np.asarray(image)).convert("L").resize((width, height), Image.BILINEAR)
    return (1.0 - np.asarray(grey, dtype=np.float32) / 255.0).astype(np.float32)