File size: 7,866 Bytes
076b9c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Stage II renderer R_psi (Sec. 3.2, Fig. 3): a NAF U-Net whose bottleneck stacks defocus residual groups (DRGs).

Inputs, with the notation of the paper: the all-in-focus image I_AiF, the blur strength K in [0, 1], the depth
D in [0, 1] (1 = far; inverted, min-max normalized Depth Anything V2 output) and the focal depth D_f = D(p_f).
The condition maps M_C (Eq. 6) are the signed defocus M^s = D - D_f (Eq. 5), the mask M^fg = 1[M^s <= 0] of the focal
plane and what lies before it (dilated by a 5x5 max filter) and the constant map K; every NAF block and every DRG
reads M_C at its resolution, and the defocus-bias attention reads the magnitude M = |M^s|. The output is
I_bokeh = I_AiF + Delta I."""
import torch
import torch.nn.functional as F
from torch import nn
from torch.nn.init import trunc_normal_

from ..layers import NAFBlock
from .defocus_bias_attention import DefocusResidualGroup

COND_CHANNELS = 3   # M_C = [M^s, M^fg, K]
RGB_MEAN = (0.4488, 0.4371, 0.4040)


def constant_map(like, strength):
    """K as a constant (b, 1, h, w) map of the size of `like`."""
    return strength.to(device=like.device, dtype=like.dtype).view(like.size(0), 1, 1, 1).expand(-1, -1, *like.shape[-2:])


def resize_bilinear(x, size):
    return F.interpolate(x, size=size, mode="bilinear", align_corners=False)


def condition_maps(depth, focal_depth, strength, size):
    """M^s = D - D_f and M_C = [M^s, M^fg, K] at `size` (the size of depth)."""
    signed_defocus = depth - focal_depth.to(depth).view(-1, 1, 1, 1)                                     # M^s
    front_mask = F.max_pool2d((signed_defocus < 1e-3).to(signed_defocus.dtype), 5, stride=1, padding=2)  # M^fg
    cond = torch.cat((resize_bilinear(signed_defocus, size), front_mask, constant_map(signed_defocus, strength)), dim=1)
    return signed_defocus, cond


class Renderer(nn.Module):
    def __init__(self, u_width=16, u_depth=2, embed_dim=96, num_groups=3, group_depth=3, num_heads=3, mlp_ratio=2,
                 drop_path_rate=0.1, noise_level=0.05):
        super().__init__()
        self.u_depth = u_depth
        self.noise_level = noise_level   # training only: Gaussian noise on the bottleneck input and output
        self.register_buffer("mean", torch.tensor(RGB_MEAN).view(1, 3, 1, 1), persistent=False)

        # input: I_AiF, M_C and the K map once more
        self.in_stage = nn.Conv2d(3 + COND_CHANNELS + 1, u_width, 3, 1, 1)
        self.in_stage_2 = nn.Conv2d(u_width, u_width, 3, 1, 1)
        self.encoders, self.downs = nn.ModuleList(), nn.ModuleList()
        chan = u_width
        for _ in range(u_depth):
            self.encoders.append(nn.ModuleList([NAFBlock(chan, COND_CHANNELS)]))
            self.downs.append(nn.Conv2d(chan, chan * 2, 2, 2))
            chan *= 2

        # bottleneck: DRGs on channels-last tokens (parameter names follow the released checkpoint)
        self.conv_prep = nn.Conv2d(chan, embed_dim, 3, 1, 1)
        self.defocus_fuse = nn.Conv2d(embed_dim + COND_CHANNELS, embed_dim, 3, 1, 1)
        self.patch_embed = nn.Module()
        self.patch_embed.norm = nn.LayerNorm(embed_dim)
        dpr = [x.item() for x in torch.linspace(0, drop_path_rate, num_groups * group_depth)]
        self.blocks = nn.ModuleList([                                       # the DRGs
            DefocusResidualGroup(embed_dim, COND_CHANNELS, group_depth, num_heads, mlp_ratio * embed_dim,
                                 dpr[i * group_depth:(i + 1) * group_depth]) for i in range(num_groups)])
        self.norm = nn.LayerNorm(embed_dim, eps=1e-6)
        self.conv_after_body = nn.Conv2d(embed_dim, embed_dim, 3, 1, 1)
        self.conv_last = nn.Conv2d(embed_dim + COND_CHANNELS, chan, 3, 1, 1)

        self.ups, self.decoders = nn.ModuleList(), nn.ModuleList()
        for level in range(u_depth):
            self.ups.append(nn.Sequential(nn.Conv2d(chan, 2 * chan, 1, bias=False), nn.PixelShuffle(2)))
            chan //= 2
            n = 4 if level == u_depth - 1 else 2
            self.decoders.append(nn.ModuleList([NAFBlock(chan, COND_CHANNELS) for _ in range(n)]))
        self.out_stage = nn.Conv2d(u_width + COND_CHANNELS, 3, 3, 1, 1)
        self.apply(self._init_weights)

    @staticmethod
    def _init_weights(m):
        if isinstance(m, nn.Linear):
            trunc_normal_(m.weight, std=0.02)
            if m.bias is not None:
                nn.init.constant_(m.bias, 0)
        elif isinstance(m, nn.LayerNorm):
            nn.init.constant_(m.bias, 0)
            nn.init.constant_(m.weight, 1.0)

    def _add_training_noise(self, x):
        return x + torch.randn_like(x) * self.noise_level if self.training and self.noise_level > 0 else x

    @staticmethod
    def _pad_to_multiple(x, multiple):
        h, w = x.shape[-2:]
        ph, pw = (multiple - h % multiple) % multiple, (multiple - w % multiple) % multiple
        if ph == 0 and pw == 0:
            return x
        return F.pad(x, (0, pw, 0, ph), "reflect" if ph < h and pw < w else "replicate")

    @staticmethod
    def _cond_at_level(cond, level):
        """M_C at U-Net level `level` (resolution 1 / 2^level)."""
        return F.interpolate(cond, scale_factor=1 / 2 ** level, mode="bilinear", align_corners=False)

    def forward(self, image, strength, depth, focal_depth):
        """image I_AiF (b, 3, h, w) in [0, 1]; strength K (b, 1); depth D (b, 1, h, w); focal_depth D_f (b, 1).
        Returns I_bokeh (b, 3, h, w)."""
        h, w = image.shape[-2:]
        strength = strength.view(-1, 1)
        multiple = 2 ** self.u_depth
        image, depth = self._pad_to_multiple(image, multiple), self._pad_to_multiple(depth.to(image), multiple)
        x = image - self.mean
        signed_defocus, cond = condition_maps(depth, focal_depth.to(image), strength, x.shape[-2:])

        x = self.in_stage_2(self.in_stage(torch.cat((x, cond, constant_map(x, strength)), dim=1)))
        skips = []
        for level, (encoder, down) in enumerate(zip(self.encoders, self.downs)):
            for block in encoder:
                x = block(x, self._cond_at_level(cond, level))
            skips.append(x)
            x = down(x)

        feat = self.conv_prep(self._add_training_noise(x))
        defocus = resize_bilinear(signed_defocus.abs().to(feat.dtype), feat.shape[-2:]).clamp(0.0, 1.0)    # M
        cond_bottleneck = self._cond_at_level(cond, self.u_depth)
        feat = self.defocus_fuse(torch.cat((feat, cond_bottleneck), dim=1))
        tokens = self.patch_embed.norm(feat.permute(0, 2, 3, 1))                                           # b, h, w, c
        for drg in self.blocks:
            tokens = drg(tokens, cond_bottleneck, defocus, strength)
        feat = self.conv_after_body(self.norm(tokens).permute(0, 3, 1, 2)) + feat
        x = self._add_training_noise(x + self.conv_last(torch.cat((feat, cond_bottleneck), dim=1)))

        for i, (decoder, up, skip) in enumerate(zip(self.decoders, self.ups, skips[::-1])):
            x = up(x) + skip
            for block in decoder:
                x = block(x, self._cond_at_level(cond, self.u_depth - 1 - i))
        residual = self.out_stage(torch.cat((x, cond), dim=1)) + self.mean                                # Delta I
        return (residual + image)[:, :, :h, :w]


def load_renderer_state(model, path):
    """Load a training checkpoint ({"model": state_dict}) or a bare state_dict strictly. Older checkpoints also store
    the table of a relative-position bias ("Relpos") that the defocus-bias attention does not use."""
    state = torch.load(path, map_location="cpu")
    state = state.get("model", state)
    state = {k.removeprefix("module."): v for k, v in state.items() if ".Relpos." not in k}
    model.load_state_dict(state, strict=True)
    return model


def load_renderer(path, device="cuda"):
    return load_renderer_state(Renderer(), path).to(device).eval()