File size: 11,766 Bytes
a64b7d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import torch
from collections import OrderedDict
from os import path as osp
from tqdm import tqdm

from basicsr.archs import build_network
from basicsr.losses import build_loss
from basicsr.metrics import calculate_metric
from basicsr.utils import imwrite, tensor2img
from basicsr.utils.registry import MODEL_REGISTRY
from .sr_model import SRModel


@MODEL_REGISTRY.register()
class HiFaceGANModel(SRModel):
    """HiFaceGAN model for generic-purpose face restoration.
    No prior modeling required, works for any degradations.
    Currently doesn't support EMA for inference.
    """

    def init_training_settings(self):

        train_opt = self.opt['train']
        self.ema_decay = train_opt.get('ema_decay', 0)
        if self.ema_decay > 0:
            raise (NotImplementedError('HiFaceGAN does not support EMA now. Pass'))

        self.net_g.train()

        self.net_d = build_network(self.opt['network_d'])
        self.net_d = self.model_to_device(self.net_d)
        self.print_network(self.net_d)

        # define losses
        # HiFaceGAN does not use pixel loss by default
        if train_opt.get('pixel_opt'):
            self.cri_pix = build_loss(train_opt['pixel_opt']).to(self.device)
        else:
            self.cri_pix = None

        if train_opt.get('perceptual_opt'):
            self.cri_perceptual = build_loss(train_opt['perceptual_opt']).to(self.device)
        else:
            self.cri_perceptual = None

        if train_opt.get('feature_matching_opt'):
            self.cri_feat = build_loss(train_opt['feature_matching_opt']).to(self.device)
        else:
            self.cri_feat = None

        if self.cri_pix is None and self.cri_perceptual is None:
            raise ValueError('Both pixel and perceptual losses are None.')

        if train_opt.get('gan_opt'):
            self.cri_gan = build_loss(train_opt['gan_opt']).to(self.device)

        self.net_d_iters = train_opt.get('net_d_iters', 1)
        self.net_d_init_iters = train_opt.get('net_d_init_iters', 0)
        # set up optimizers and schedulers
        self.setup_optimizers()
        self.setup_schedulers()

    def setup_optimizers(self):
        train_opt = self.opt['train']
        # optimizer g
        optim_type = train_opt['optim_g'].pop('type')
        self.optimizer_g = self.get_optimizer(optim_type, self.net_g.parameters(), **train_opt['optim_g'])
        self.optimizers.append(self.optimizer_g)
        # optimizer d
        optim_type = train_opt['optim_d'].pop('type')
        self.optimizer_d = self.get_optimizer(optim_type, self.net_d.parameters(), **train_opt['optim_d'])
        self.optimizers.append(self.optimizer_d)

    def discriminate(self, input_lq, output, ground_truth):
        """
        This is a conditional (on the input) discriminator
        In Batch Normalization, the fake and real images are
        recommended to be in the same batch to avoid disparate
        statistics in fake and real images.
        So both fake and real images are fed to D all at once.
        """
        h, w = output.shape[-2:]
        if output.shape[-2:] != input_lq.shape[-2:]:
            lq = torch.nn.functional.interpolate(input_lq, (h, w))
            real = torch.nn.functional.interpolate(ground_truth, (h, w))
            fake_concat = torch.cat([lq, output], dim=1)
            real_concat = torch.cat([lq, real], dim=1)
        else:
            fake_concat = torch.cat([input_lq, output], dim=1)
            real_concat = torch.cat([input_lq, ground_truth], dim=1)

        fake_and_real = torch.cat([fake_concat, real_concat], dim=0)
        discriminator_out = self.net_d(fake_and_real)
        pred_fake, pred_real = self._divide_pred(discriminator_out)
        return pred_fake, pred_real

    @staticmethod
    def _divide_pred(pred):
        """
        Take the prediction of fake and real images from the combined batch.
        The prediction contains the intermediate outputs of multiscale GAN,
        so it's usually a list
        """
        if type(pred) == list:
            fake = []
            real = []
            for p in pred:
                fake.append([tensor[:tensor.size(0) // 2] for tensor in p])
                real.append([tensor[tensor.size(0) // 2:] for tensor in p])
        else:
            fake = pred[:pred.size(0) // 2]
            real = pred[pred.size(0) // 2:]

        return fake, real

    def optimize_parameters(self, current_iter):
        # optimize net_g
        for p in self.net_d.parameters():
            p.requires_grad = False

        self.optimizer_g.zero_grad()
        self.output = self.net_g(self.lq)

        l_g_total = 0
        loss_dict = OrderedDict()

        if (current_iter % self.net_d_iters == 0 and current_iter > self.net_d_init_iters):
            # pixel loss
            if self.cri_pix:
                l_g_pix = self.cri_pix(self.output, self.gt)
                l_g_total += l_g_pix
                loss_dict['l_g_pix'] = l_g_pix

            # perceptual loss
            if self.cri_perceptual:
                l_g_percep, l_g_style = self.cri_perceptual(self.output, self.gt)
                if l_g_percep is not None:
                    l_g_total += l_g_percep
                    loss_dict['l_g_percep'] = l_g_percep
                if l_g_style is not None:
                    l_g_total += l_g_style
                    loss_dict['l_g_style'] = l_g_style

            # Requires real prediction for feature matching loss
            pred_fake, pred_real = self.discriminate(self.lq, self.output, self.gt)
            l_g_gan = self.cri_gan(pred_fake, True, is_disc=False)
            l_g_total += l_g_gan
            loss_dict['l_g_gan'] = l_g_gan

            # feature matching loss
            if self.cri_feat:
                l_g_feat = self.cri_feat(pred_fake, pred_real)
                l_g_total += l_g_feat
                loss_dict['l_g_feat'] = l_g_feat

            l_g_total.backward()
            self.optimizer_g.step()

        # optimize net_d
        for p in self.net_d.parameters():
            p.requires_grad = True

        self.optimizer_d.zero_grad()
        # TODO: Benchmark test between HiFaceGAN and SRGAN implementation:
        # SRGAN use the same fake output for discriminator update
        # while HiFaceGAN regenerate a new output using updated net_g
        # This should not make too much difference though. Stick to SRGAN now.
        # -------------------------------------------------------------------
        # ---------- Below are original HiFaceGAN code snippet --------------
        # -------------------------------------------------------------------
        # with torch.no_grad():
        #    fake_image = self.net_g(self.lq)
        #    fake_image = fake_image.detach()
        #    fake_image.requires_grad_()
        #    pred_fake, pred_real = self.discriminate(self.lq, fake_image, self.gt)

        # real
        pred_fake, pred_real = self.discriminate(self.lq, self.output.detach(), self.gt)
        l_d_real = self.cri_gan(pred_real, True, is_disc=True)
        loss_dict['l_d_real'] = l_d_real
        # fake
        l_d_fake = self.cri_gan(pred_fake, False, is_disc=True)
        loss_dict['l_d_fake'] = l_d_fake

        l_d_total = (l_d_real + l_d_fake) / 2
        l_d_total.backward()
        self.optimizer_d.step()

        self.log_dict = self.reduce_loss_dict(loss_dict)

        if self.ema_decay > 0:
            print('HiFaceGAN does not support EMA now. pass')

    def validation(self, dataloader, current_iter, tb_logger, save_img=False):
        """
        Warning: HiFaceGAN requires train() mode even for validation
        For more info, see https://github.com/Lotayou/Face-Renovation/issues/31

        Args:
            dataloader (torch.utils.data.DataLoader): Validation dataloader.
            current_iter (int): Current iteration.
            tb_logger (tensorboard logger): Tensorboard logger.
            save_img (bool): Whether to save images. Default: False.
        """

        if self.opt['network_g']['type'] in ('HiFaceGAN', 'SPADEGenerator'):
            self.net_g.train()

        if self.opt['dist']:
            self.dist_validation(dataloader, current_iter, tb_logger, save_img)
        else:
            print('In HiFaceGANModel: The new metrics package is under development.' +
                  'Using super method now (Only PSNR & SSIM are supported)')
            super().nondist_validation(dataloader, current_iter, tb_logger, save_img)

    def nondist_validation(self, dataloader, current_iter, tb_logger, save_img):
        """
        TODO: Validation using updated metric system
        The metrics are now evaluated after all images have been tested
        This allows batch processing, and also allows evaluation of
        distributional metrics, such as:

        @ Frechet Inception Distance: FID
        @ Maximum Mean Discrepancy: MMD

        Warning:
            Need careful batch management for different inference settings.

        """
        dataset_name = dataloader.dataset.opt['name']
        with_metrics = self.opt['val'].get('metrics') is not None
        if with_metrics:
            self.metric_results = dict()  # {metric: 0 for metric in self.opt['val']['metrics'].keys()}
            sr_tensors = []
            gt_tensors = []

        pbar = tqdm(total=len(dataloader), unit='image')
        for val_data in dataloader:
            img_name = osp.splitext(osp.basename(val_data['lq_path'][0]))[0]
            self.feed_data(val_data)
            self.test()

            visuals = self.get_current_visuals()  # detached cpu tensor, non-squeeze
            sr_tensors.append(visuals['result'])
            if 'gt' in visuals:
                gt_tensors.append(visuals['gt'])
                del self.gt

            # tentative for out of GPU memory
            del self.lq
            del self.output
            torch.cuda.empty_cache()

            if save_img:
                if self.opt['is_train']:
                    save_img_path = osp.join(self.opt['path']['visualization'], img_name,
                                             f'{img_name}_{current_iter}.png')
                else:
                    if self.opt['val']['suffix']:
                        save_img_path = osp.join(self.opt['path']['visualization'], dataset_name,
                                                 f'{img_name}_{self.opt["val"]["suffix"]}.png')
                    else:
                        save_img_path = osp.join(self.opt['path']['visualization'], dataset_name,
                                                 f'{img_name}_{self.opt["name"]}.png')

                imwrite(tensor2img(visuals['result']), save_img_path)

            pbar.update(1)
            pbar.set_description(f'Test {img_name}')
        pbar.close()

        if with_metrics:
            sr_pack = torch.cat(sr_tensors, dim=0)
            gt_pack = torch.cat(gt_tensors, dim=0)
            # calculate metrics
            for name, opt_ in self.opt['val']['metrics'].items():
                # The new metric caller automatically returns mean value
                # FIXME: ERROR: calculate_metric only supports two arguments. Now the codes cannot be successfully run
                self.metric_results[name] = calculate_metric(dict(sr_pack=sr_pack, gt_pack=gt_pack), opt_)
            self._log_validation_metric_values(current_iter, dataset_name, tb_logger)

    def save(self, epoch, current_iter):
        if hasattr(self, 'net_g_ema'):
            print('HiFaceGAN does not support EMA now. Fallback to normal mode.')

        self.save_network(self.net_g, 'net_g', current_iter)
        self.save_network(self.net_d, 'net_d', current_iter)
        self.save_training_state(epoch, current_iter)