add files
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -0,0 +1,146 @@
|
||||
import os
|
||||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
from omegaconf import OmegaConf
|
||||
from pathlib import Path
|
||||
|
||||
import torch.nn as nn
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from saicinpainting.evaluation.utils import move_to_device
|
||||
from saicinpainting.training.trainers import load_checkpoint
|
||||
from saicinpainting.evaluation.data import pad_tensor_to_modulo
|
||||
|
||||
|
||||
MODELS_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "models")
|
||||
CONFIG_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "config")
|
||||
|
||||
|
||||
|
||||
def inpaint_img_with_lama(
|
||||
img,
|
||||
mask,
|
||||
config_p: OmegaConf,
|
||||
model,
|
||||
mod=8,
|
||||
device="cuda"
|
||||
):
|
||||
batch = {}
|
||||
print(img.shape)
|
||||
print(mask.shape)
|
||||
mask=mask*255
|
||||
batch['image'] = img.permute(0,3, 1, 2)
|
||||
batch['mask'] = mask[None, None]
|
||||
unpad_to_size = [batch['image'].shape[2], batch['image'].shape[3]]
|
||||
batch['image'] = pad_tensor_to_modulo(batch['image'], mod)
|
||||
batch['mask'] = pad_tensor_to_modulo(batch['mask'], mod)
|
||||
batch = move_to_device(batch, device)
|
||||
batch['mask'] = (batch['mask'] > 0) * 1
|
||||
|
||||
batch = model(batch)
|
||||
cur_res = batch[config_p.out_key][0].permute(1, 2, 0)
|
||||
cur_res = cur_res.detach().cpu().numpy()
|
||||
|
||||
if unpad_to_size is not None:
|
||||
orig_height, orig_width = unpad_to_size
|
||||
cur_res = cur_res[:orig_height, :orig_width]
|
||||
|
||||
#cur_res = np.clip(cur_res * 255, 0, 255).astype('uint8')
|
||||
return cur_res
|
||||
|
||||
class LamaApply:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"lama":("LAMA",),
|
||||
"config":("YAML_CONFIG",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
|
||||
FUNCTION = "lama_remove"
|
||||
|
||||
CATEGORY = "lama"
|
||||
|
||||
def lama_remove(self,image,mask,config,lama):
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
img_inpainted = inpaint_img_with_lama(
|
||||
image, mask, config, lama, device=device)
|
||||
img = torch.from_numpy(img_inpainted)[None,]
|
||||
return (img,)
|
||||
|
||||
|
||||
class LamaModelLoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required":{
|
||||
"config":("YAML_CONFIG",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LAMA","YAML_CONFIG")
|
||||
|
||||
FUNCTION = "load_lama"
|
||||
|
||||
CATEGORY = "lama"
|
||||
def load_lama(self,config):
|
||||
device = torch.device(config.device)
|
||||
config.training_model.predict_only = True
|
||||
config.visualizer.kind = 'noop'
|
||||
checkpoint_path = os.path.join(MODELS_DIR,config.model.checkpoint)
|
||||
model = load_checkpoint(config, checkpoint_path, strict=False)
|
||||
model.to(device)
|
||||
model.freeze()
|
||||
return (model,config)
|
||||
|
||||
|
||||
class YamlConfigLoader:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
files = [f for f in os.listdir(CONFIG_DIR) if f.endswith('.yaml')]
|
||||
return {
|
||||
"required":{
|
||||
"yaml_config": (files,),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("YAML_CONFIG",)
|
||||
|
||||
FUNCTION = "load_yaml"
|
||||
|
||||
CATEGORY = "load_yaml"
|
||||
|
||||
def load_yaml(self,yaml_config):
|
||||
yaml_path=os.path.join(CONFIG_DIR, yaml_config)
|
||||
config = OmegaConf.load(yaml_path)
|
||||
return (config,)
|
||||
|
||||
|
||||
|
||||
# A dictionary that contains all nodes you want to export with their names
|
||||
# NOTE: names should be globally unique
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LamaModelLoader":LamaModelLoader,
|
||||
"LamaApply": LamaApply,
|
||||
"YamlConfigLoader":YamlConfigLoader,
|
||||
}
|
||||
|
||||
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LamaModelLoader":"LamaModelLoader",
|
||||
"LamaApply": "LamaApply",
|
||||
"YamlConfigLoader":"YamlConfigLoader"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
from .LamaRemove import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .saicinpainting.training.data.datasets import make_constant_area_crop_params
|
||||
from .saicinpainting.training.losses.distance_weighting import make_mask_distance_weighter
|
||||
from .saicinpainting.training.losses.feature_matching import feature_matching_loss, masked_l1_loss
|
||||
from .saicinpainting.training.modules.fake_fakes import FakeFakesGenerator
|
||||
from .saicinpainting.training.trainers.base import BaseInpaintingTrainingModule, make_multiscale_noise
|
||||
from .saicinpainting.utils import add_prefix_to_keys, get_ramp
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
@@ -0,0 +1,183 @@
|
||||
indir: no # to be overriden in CLI
|
||||
outdir: no # to be overriden in CLI
|
||||
|
||||
model:
|
||||
path: no # to be overriden in CLI
|
||||
checkpoint: big-lama.ckpt
|
||||
|
||||
dataset:
|
||||
kind: default
|
||||
img_suffix: .png
|
||||
pad_out_to_modulo: 8
|
||||
|
||||
device: cuda
|
||||
out_key: inpainted
|
||||
|
||||
refine: False # refiner will only run if this is True
|
||||
refiner:
|
||||
gpu_ids: 0,1 # the GPU ids of the machine to use. If only single GPU, use: "0,"
|
||||
modulo: ${dataset.pad_out_to_modulo}
|
||||
n_iters: 15 # number of iterations of refinement for each scale
|
||||
lr: 0.002 # learning rate
|
||||
min_side: 512 # all sides of image on all scales should be >= min_side / sqrt(2)
|
||||
max_scales: 3 # max number of downscaling scales for the image-mask pyramid
|
||||
px_budget: 1800000 # pixels budget. Any image will be resized to satisfy height*width <= px_budget
|
||||
|
||||
|
||||
run_title: b18_ffc075_batch8x15
|
||||
training_model:
|
||||
kind: default
|
||||
visualize_each_iters: 1000
|
||||
concat_mask: true
|
||||
store_discr_outputs_for_vis: true
|
||||
losses:
|
||||
l1:
|
||||
weight_missing: 0
|
||||
weight_known: 10
|
||||
perceptual:
|
||||
weight: 0
|
||||
adversarial:
|
||||
kind: r1
|
||||
weight: 10
|
||||
gp_coef: 0.001
|
||||
mask_as_fake_target: true
|
||||
allow_scale_mask: true
|
||||
feature_matching:
|
||||
weight: 100
|
||||
resnet_pl:
|
||||
weight: 30
|
||||
weights_path: ${oc.env:TORCH_HOME}
|
||||
|
||||
optimizers:
|
||||
generator:
|
||||
kind: adam
|
||||
lr: 0.001
|
||||
discriminator:
|
||||
kind: adam
|
||||
lr: 0.0001
|
||||
visualizer:
|
||||
key_order:
|
||||
- image
|
||||
- predicted_image
|
||||
- discr_output_fake
|
||||
- discr_output_real
|
||||
- inpainted
|
||||
rescale_keys:
|
||||
- discr_output_fake
|
||||
- discr_output_real
|
||||
kind: directory
|
||||
outdir: /group-volume/User-Driven-Content-Generation/r.suvorov/inpainting/experiments/r.suvorov_2021-04-30_14-41-12_train_simple_pix2pix2_gap_sdpl_novgg_large_b18_ffc075_batch8x15/samples
|
||||
location:
|
||||
data_root_dir: /group-volume/User-Driven-Content-Generation/datasets/inpainting_data_root_large
|
||||
out_root_dir: /group-volume/User-Driven-Content-Generation/${env:USER}/inpainting/experiments
|
||||
tb_dir: /group-volume/User-Driven-Content-Generation/${env:USER}/inpainting/tb_logs
|
||||
data:
|
||||
batch_size: 15
|
||||
val_batch_size: 2
|
||||
num_workers: 3
|
||||
train:
|
||||
indir: ${location.data_root_dir}/train
|
||||
out_size: 256
|
||||
mask_gen_kwargs:
|
||||
irregular_proba: 1
|
||||
irregular_kwargs:
|
||||
max_angle: 4
|
||||
max_len: 200
|
||||
max_width: 100
|
||||
max_times: 5
|
||||
min_times: 1
|
||||
box_proba: 1
|
||||
box_kwargs:
|
||||
margin: 10
|
||||
bbox_min_size: 30
|
||||
bbox_max_size: 150
|
||||
max_times: 3
|
||||
min_times: 1
|
||||
segm_proba: 0
|
||||
segm_kwargs:
|
||||
confidence_threshold: 0.5
|
||||
max_object_area: 0.5
|
||||
min_mask_area: 0.07
|
||||
downsample_levels: 6
|
||||
num_variants_per_mask: 1
|
||||
rigidness_mode: 1
|
||||
max_foreground_coverage: 0.3
|
||||
max_foreground_intersection: 0.7
|
||||
max_mask_intersection: 0.1
|
||||
max_hidden_area: 0.1
|
||||
max_scale_change: 0.25
|
||||
horizontal_flip: true
|
||||
max_vertical_shift: 0.2
|
||||
position_shuffle: true
|
||||
transform_variant: distortions
|
||||
dataloader_kwargs:
|
||||
batch_size: ${data.batch_size}
|
||||
shuffle: true
|
||||
num_workers: ${data.num_workers}
|
||||
val:
|
||||
indir: ${location.data_root_dir}/val
|
||||
img_suffix: .png
|
||||
dataloader_kwargs:
|
||||
batch_size: ${data.val_batch_size}
|
||||
shuffle: false
|
||||
num_workers: ${data.num_workers}
|
||||
visual_test:
|
||||
indir: ${location.data_root_dir}/korean_test
|
||||
img_suffix: _input.png
|
||||
pad_out_to_modulo: 32
|
||||
dataloader_kwargs:
|
||||
batch_size: 1
|
||||
shuffle: false
|
||||
num_workers: ${data.num_workers}
|
||||
generator:
|
||||
kind: ffc_resnet
|
||||
input_nc: 4
|
||||
output_nc: 3
|
||||
ngf: 64
|
||||
n_downsampling: 3
|
||||
n_blocks: 18
|
||||
add_out_act: sigmoid
|
||||
init_conv_kwargs:
|
||||
ratio_gin: 0
|
||||
ratio_gout: 0
|
||||
enable_lfu: false
|
||||
downsample_conv_kwargs:
|
||||
ratio_gin: ${generator.init_conv_kwargs.ratio_gout}
|
||||
ratio_gout: ${generator.downsample_conv_kwargs.ratio_gin}
|
||||
enable_lfu: false
|
||||
resnet_conv_kwargs:
|
||||
ratio_gin: 0.75
|
||||
ratio_gout: ${generator.resnet_conv_kwargs.ratio_gin}
|
||||
enable_lfu: false
|
||||
discriminator:
|
||||
kind: pix2pixhd_nlayer
|
||||
input_nc: 3
|
||||
ndf: 64
|
||||
n_layers: 4
|
||||
evaluator:
|
||||
kind: default
|
||||
inpainted_key: inpainted
|
||||
integral_kind: ssim_fid100_f1
|
||||
trainer:
|
||||
kwargs:
|
||||
gpus: -1
|
||||
accelerator: ddp
|
||||
max_epochs: 200
|
||||
gradient_clip_val: 1
|
||||
log_gpu_memory: None
|
||||
limit_train_batches: 25000
|
||||
val_check_interval: ${trainer.kwargs.limit_train_batches}
|
||||
log_every_n_steps: 1000
|
||||
precision: 32
|
||||
terminate_on_nan: false
|
||||
check_val_every_n_epoch: 1
|
||||
num_sanity_val_steps: 8
|
||||
limit_val_batches: 1000
|
||||
replace_sampler_ddp: false
|
||||
checkpoint_kwargs:
|
||||
verbose: true
|
||||
save_top_k: 5
|
||||
save_last: true
|
||||
period: 1
|
||||
monitor: val_ssim_fid100_f1_total_mean
|
||||
mode: max
|
||||
@@ -0,0 +1,175 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
from saicinpainting.training.data.datasets import make_constant_area_crop_params
|
||||
from saicinpainting.training.losses.distance_weighting import make_mask_distance_weighter
|
||||
from saicinpainting.training.losses.feature_matching import feature_matching_loss, masked_l1_loss
|
||||
from saicinpainting.training.modules.fake_fakes import FakeFakesGenerator
|
||||
from saicinpainting.training.trainers.base import BaseInpaintingTrainingModule, make_multiscale_noise
|
||||
from saicinpainting.utils import add_prefix_to_keys, get_ramp
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_constant_area_crop_batch(batch, **kwargs):
|
||||
crop_y, crop_x, crop_height, crop_width = make_constant_area_crop_params(img_height=batch['image'].shape[2],
|
||||
img_width=batch['image'].shape[3],
|
||||
**kwargs)
|
||||
batch['image'] = batch['image'][:, :, crop_y : crop_y + crop_height, crop_x : crop_x + crop_width]
|
||||
batch['mask'] = batch['mask'][:, :, crop_y: crop_y + crop_height, crop_x: crop_x + crop_width]
|
||||
return batch
|
||||
|
||||
|
||||
class DefaultInpaintingTrainingModule(BaseInpaintingTrainingModule):
|
||||
def __init__(self, *args, concat_mask=True, rescale_scheduler_kwargs=None, image_to_discriminator='predicted_image',
|
||||
add_noise_kwargs=None, noise_fill_hole=False, const_area_crop_kwargs=None,
|
||||
distance_weighter_kwargs=None, distance_weighted_mask_for_discr=False,
|
||||
fake_fakes_proba=0, fake_fakes_generator_kwargs=None,
|
||||
**kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.concat_mask = concat_mask
|
||||
self.rescale_size_getter = get_ramp(**rescale_scheduler_kwargs) if rescale_scheduler_kwargs is not None else None
|
||||
self.image_to_discriminator = image_to_discriminator
|
||||
self.add_noise_kwargs = add_noise_kwargs
|
||||
self.noise_fill_hole = noise_fill_hole
|
||||
self.const_area_crop_kwargs = const_area_crop_kwargs
|
||||
self.refine_mask_for_losses = make_mask_distance_weighter(**distance_weighter_kwargs) \
|
||||
if distance_weighter_kwargs is not None else None
|
||||
self.distance_weighted_mask_for_discr = distance_weighted_mask_for_discr
|
||||
|
||||
self.fake_fakes_proba = fake_fakes_proba
|
||||
if self.fake_fakes_proba > 1e-3:
|
||||
self.fake_fakes_gen = FakeFakesGenerator(**(fake_fakes_generator_kwargs or {}))
|
||||
|
||||
def forward(self, batch):
|
||||
if self.training and self.rescale_size_getter is not None:
|
||||
cur_size = self.rescale_size_getter(self.global_step)
|
||||
batch['image'] = F.interpolate(batch['image'], size=cur_size, mode='bilinear', align_corners=False)
|
||||
batch['mask'] = F.interpolate(batch['mask'], size=cur_size, mode='nearest')
|
||||
|
||||
if self.training and self.const_area_crop_kwargs is not None:
|
||||
batch = make_constant_area_crop_batch(batch, **self.const_area_crop_kwargs)
|
||||
|
||||
img = batch['image']
|
||||
mask = batch['mask']
|
||||
|
||||
masked_img = img * (1 - mask)
|
||||
|
||||
if self.add_noise_kwargs is not None:
|
||||
noise = make_multiscale_noise(masked_img, **self.add_noise_kwargs)
|
||||
if self.noise_fill_hole:
|
||||
masked_img = masked_img + mask * noise[:, :masked_img.shape[1]]
|
||||
masked_img = torch.cat([masked_img, noise], dim=1)
|
||||
|
||||
if self.concat_mask:
|
||||
masked_img = torch.cat([masked_img, mask], dim=1)
|
||||
|
||||
batch['predicted_image'] = self.generator(masked_img)
|
||||
batch['inpainted'] = mask * batch['predicted_image'] + (1 - mask) * batch['image']
|
||||
|
||||
if self.fake_fakes_proba > 1e-3:
|
||||
if self.training and torch.rand(1).item() < self.fake_fakes_proba:
|
||||
batch['fake_fakes'], batch['fake_fakes_masks'] = self.fake_fakes_gen(img, mask)
|
||||
batch['use_fake_fakes'] = True
|
||||
else:
|
||||
batch['fake_fakes'] = torch.zeros_like(img)
|
||||
batch['fake_fakes_masks'] = torch.zeros_like(mask)
|
||||
batch['use_fake_fakes'] = False
|
||||
|
||||
batch['mask_for_losses'] = self.refine_mask_for_losses(img, batch['predicted_image'], mask) \
|
||||
if self.refine_mask_for_losses is not None and self.training \
|
||||
else mask
|
||||
|
||||
return batch
|
||||
|
||||
def generator_loss(self, batch):
|
||||
img = batch['image']
|
||||
predicted_img = batch[self.image_to_discriminator]
|
||||
original_mask = batch['mask']
|
||||
supervised_mask = batch['mask_for_losses']
|
||||
|
||||
# L1
|
||||
l1_value = masked_l1_loss(predicted_img, img, supervised_mask,
|
||||
self.config.losses.l1.weight_known,
|
||||
self.config.losses.l1.weight_missing)
|
||||
|
||||
total_loss = l1_value
|
||||
metrics = dict(gen_l1=l1_value)
|
||||
|
||||
# vgg-based perceptual loss
|
||||
if self.config.losses.perceptual.weight > 0:
|
||||
pl_value = self.loss_pl(predicted_img, img, mask=supervised_mask).sum() * self.config.losses.perceptual.weight
|
||||
total_loss = total_loss + pl_value
|
||||
metrics['gen_pl'] = pl_value
|
||||
|
||||
# discriminator
|
||||
# adversarial_loss calls backward by itself
|
||||
mask_for_discr = supervised_mask if self.distance_weighted_mask_for_discr else original_mask
|
||||
self.adversarial_loss.pre_generator_step(real_batch=img, fake_batch=predicted_img,
|
||||
generator=self.generator, discriminator=self.discriminator)
|
||||
discr_real_pred, discr_real_features = self.discriminator(img)
|
||||
discr_fake_pred, discr_fake_features = self.discriminator(predicted_img)
|
||||
adv_gen_loss, adv_metrics = self.adversarial_loss.generator_loss(real_batch=img,
|
||||
fake_batch=predicted_img,
|
||||
discr_real_pred=discr_real_pred,
|
||||
discr_fake_pred=discr_fake_pred,
|
||||
mask=mask_for_discr)
|
||||
total_loss = total_loss + adv_gen_loss
|
||||
metrics['gen_adv'] = adv_gen_loss
|
||||
metrics.update(add_prefix_to_keys(adv_metrics, 'adv_'))
|
||||
|
||||
# feature matching
|
||||
if self.config.losses.feature_matching.weight > 0:
|
||||
need_mask_in_fm = OmegaConf.to_container(self.config.losses.feature_matching).get('pass_mask', False)
|
||||
mask_for_fm = supervised_mask if need_mask_in_fm else None
|
||||
fm_value = feature_matching_loss(discr_fake_features, discr_real_features,
|
||||
mask=mask_for_fm) * self.config.losses.feature_matching.weight
|
||||
total_loss = total_loss + fm_value
|
||||
metrics['gen_fm'] = fm_value
|
||||
|
||||
if self.loss_resnet_pl is not None:
|
||||
resnet_pl_value = self.loss_resnet_pl(predicted_img, img)
|
||||
total_loss = total_loss + resnet_pl_value
|
||||
metrics['gen_resnet_pl'] = resnet_pl_value
|
||||
|
||||
return total_loss, metrics
|
||||
|
||||
def discriminator_loss(self, batch):
|
||||
total_loss = 0
|
||||
metrics = {}
|
||||
|
||||
predicted_img = batch[self.image_to_discriminator].detach()
|
||||
self.adversarial_loss.pre_discriminator_step(real_batch=batch['image'], fake_batch=predicted_img,
|
||||
generator=self.generator, discriminator=self.discriminator)
|
||||
discr_real_pred, discr_real_features = self.discriminator(batch['image'])
|
||||
discr_fake_pred, discr_fake_features = self.discriminator(predicted_img)
|
||||
adv_discr_loss, adv_metrics = self.adversarial_loss.discriminator_loss(real_batch=batch['image'],
|
||||
fake_batch=predicted_img,
|
||||
discr_real_pred=discr_real_pred,
|
||||
discr_fake_pred=discr_fake_pred,
|
||||
mask=batch['mask'])
|
||||
total_loss = total_loss + adv_discr_loss
|
||||
metrics['discr_adv'] = adv_discr_loss
|
||||
metrics.update(add_prefix_to_keys(adv_metrics, 'adv_'))
|
||||
|
||||
|
||||
if batch.get('use_fake_fakes', False):
|
||||
fake_fakes = batch['fake_fakes']
|
||||
self.adversarial_loss.pre_discriminator_step(real_batch=batch['image'], fake_batch=fake_fakes,
|
||||
generator=self.generator, discriminator=self.discriminator)
|
||||
discr_fake_fakes_pred, _ = self.discriminator(fake_fakes)
|
||||
fake_fakes_adv_discr_loss, fake_fakes_adv_metrics = self.adversarial_loss.discriminator_loss(
|
||||
real_batch=batch['image'],
|
||||
fake_batch=fake_fakes,
|
||||
discr_real_pred=discr_real_pred,
|
||||
discr_fake_pred=discr_fake_fakes_pred,
|
||||
mask=batch['mask']
|
||||
)
|
||||
total_loss = total_loss + fake_fakes_adv_discr_loss
|
||||
metrics['discr_adv_fake_fakes'] = fake_fakes_adv_discr_loss
|
||||
metrics.update(add_prefix_to_keys(fake_fakes_adv_metrics, 'adv_'))
|
||||
|
||||
return total_loss, metrics
|
||||
@@ -0,0 +1,19 @@
|
||||
pyyaml
|
||||
tqdm
|
||||
numpy
|
||||
easydict==1.9.0
|
||||
scikit-image
|
||||
scikit-learn
|
||||
opencv-python
|
||||
tensorflow
|
||||
joblib
|
||||
matplotlib
|
||||
pandas
|
||||
albumentations==0.5.2
|
||||
hydra-core==1.1.0
|
||||
pytorch-lightning==1.2.9
|
||||
tabulate
|
||||
kornia==0.5.0
|
||||
webdataset
|
||||
packaging
|
||||
wldhx.yadisk-direct
|
||||
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
from saicinpainting.evaluation.evaluator import InpaintingEvaluatorOnline, ssim_fid100_f1, lpips_fid100_f1
|
||||
from saicinpainting.evaluation.losses.base_loss import SSIMScore, LPIPSScore, FIDScore
|
||||
|
||||
|
||||
def make_evaluator(kind='default', ssim=True, lpips=True, fid=True, integral_kind=None, **kwargs):
|
||||
logging.info(f'Make evaluator {kind}')
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
metrics = {}
|
||||
if ssim:
|
||||
metrics['ssim'] = SSIMScore()
|
||||
if lpips:
|
||||
metrics['lpips'] = LPIPSScore()
|
||||
if fid:
|
||||
metrics['fid'] = FIDScore().to(device)
|
||||
|
||||
if integral_kind is None:
|
||||
integral_func = None
|
||||
elif integral_kind == 'ssim_fid100_f1':
|
||||
integral_func = ssim_fid100_f1
|
||||
elif integral_kind == 'lpips_fid100_f1':
|
||||
integral_func = lpips_fid100_f1
|
||||
else:
|
||||
raise ValueError(f'Unexpected integral_kind={integral_kind}')
|
||||
|
||||
if kind == 'default':
|
||||
return InpaintingEvaluatorOnline(scores=metrics,
|
||||
integral_func=integral_func,
|
||||
integral_title=integral_kind,
|
||||
**kwargs)
|
||||
@@ -0,0 +1,168 @@
|
||||
import glob
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import PIL.Image as Image
|
||||
import numpy as np
|
||||
|
||||
from torch.utils.data import Dataset
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def load_image(fname, mode='RGB', return_orig=False):
|
||||
img = np.array(Image.open(fname).convert(mode))
|
||||
if img.ndim == 3:
|
||||
img = np.transpose(img, (2, 0, 1))
|
||||
out_img = img.astype('float32') / 255
|
||||
if return_orig:
|
||||
return out_img, img
|
||||
else:
|
||||
return out_img
|
||||
|
||||
|
||||
def ceil_modulo(x, mod):
|
||||
if x % mod == 0:
|
||||
return x
|
||||
return (x // mod + 1) * mod
|
||||
|
||||
|
||||
def pad_img_to_modulo(img, mod):
|
||||
channels, height, width = img.shape
|
||||
out_height = ceil_modulo(height, mod)
|
||||
out_width = ceil_modulo(width, mod)
|
||||
return np.pad(img, ((0, 0), (0, out_height - height), (0, out_width - width)), mode='symmetric')
|
||||
|
||||
|
||||
def pad_tensor_to_modulo(img, mod):
|
||||
batch_size, channels, height, width = img.shape
|
||||
out_height = ceil_modulo(height, mod)
|
||||
out_width = ceil_modulo(width, mod)
|
||||
return F.pad(img, pad=(0, out_width - width, 0, out_height - height), mode='reflect')
|
||||
|
||||
|
||||
def scale_image(img, factor, interpolation=cv2.INTER_AREA):
|
||||
if img.shape[0] == 1:
|
||||
img = img[0]
|
||||
else:
|
||||
img = np.transpose(img, (1, 2, 0))
|
||||
|
||||
img = cv2.resize(img, dsize=None, fx=factor, fy=factor, interpolation=interpolation)
|
||||
|
||||
if img.ndim == 2:
|
||||
img = img[None, ...]
|
||||
else:
|
||||
img = np.transpose(img, (2, 0, 1))
|
||||
return img
|
||||
|
||||
|
||||
class InpaintingDataset(Dataset):
|
||||
def __init__(self, datadir, img_suffix='.jpg', pad_out_to_modulo=None, scale_factor=None):
|
||||
self.datadir = datadir
|
||||
self.mask_filenames = sorted(list(glob.glob(os.path.join(self.datadir, '**', '*mask*.png'), recursive=True)))
|
||||
self.img_filenames = [fname.rsplit('_mask', 1)[0] + img_suffix for fname in self.mask_filenames]
|
||||
self.pad_out_to_modulo = pad_out_to_modulo
|
||||
self.scale_factor = scale_factor
|
||||
|
||||
def __len__(self):
|
||||
return len(self.mask_filenames)
|
||||
|
||||
def __getitem__(self, i):
|
||||
image = load_image(self.img_filenames[i], mode='RGB')
|
||||
mask = load_image(self.mask_filenames[i], mode='L')
|
||||
result = dict(image=image, mask=mask[None, ...])
|
||||
|
||||
if self.scale_factor is not None:
|
||||
result['image'] = scale_image(result['image'], self.scale_factor)
|
||||
result['mask'] = scale_image(result['mask'], self.scale_factor, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
if self.pad_out_to_modulo is not None and self.pad_out_to_modulo > 1:
|
||||
result['unpad_to_size'] = result['image'].shape[1:]
|
||||
result['image'] = pad_img_to_modulo(result['image'], self.pad_out_to_modulo)
|
||||
result['mask'] = pad_img_to_modulo(result['mask'], self.pad_out_to_modulo)
|
||||
|
||||
return result
|
||||
|
||||
class OurInpaintingDataset(Dataset):
|
||||
def __init__(self, datadir, img_suffix='.jpg', pad_out_to_modulo=None, scale_factor=None):
|
||||
self.datadir = datadir
|
||||
self.mask_filenames = sorted(list(glob.glob(os.path.join(self.datadir, 'mask', '**', '*mask*.png'), recursive=True)))
|
||||
self.img_filenames = [os.path.join(self.datadir, 'img', os.path.basename(fname.rsplit('-', 1)[0].rsplit('_', 1)[0]) + '.png') for fname in self.mask_filenames]
|
||||
self.pad_out_to_modulo = pad_out_to_modulo
|
||||
self.scale_factor = scale_factor
|
||||
|
||||
def __len__(self):
|
||||
return len(self.mask_filenames)
|
||||
|
||||
def __getitem__(self, i):
|
||||
result = dict(image=load_image(self.img_filenames[i], mode='RGB'),
|
||||
mask=load_image(self.mask_filenames[i], mode='L')[None, ...])
|
||||
|
||||
if self.scale_factor is not None:
|
||||
result['image'] = scale_image(result['image'], self.scale_factor)
|
||||
result['mask'] = scale_image(result['mask'], self.scale_factor)
|
||||
|
||||
if self.pad_out_to_modulo is not None and self.pad_out_to_modulo > 1:
|
||||
result['image'] = pad_img_to_modulo(result['image'], self.pad_out_to_modulo)
|
||||
result['mask'] = pad_img_to_modulo(result['mask'], self.pad_out_to_modulo)
|
||||
|
||||
return result
|
||||
|
||||
class PrecomputedInpaintingResultsDataset(InpaintingDataset):
|
||||
def __init__(self, datadir, predictdir, inpainted_suffix='_inpainted.jpg', **kwargs):
|
||||
super().__init__(datadir, **kwargs)
|
||||
if not datadir.endswith('/'):
|
||||
datadir += '/'
|
||||
self.predictdir = predictdir
|
||||
self.pred_filenames = [os.path.join(predictdir, os.path.splitext(fname[len(datadir):])[0] + inpainted_suffix)
|
||||
for fname in self.mask_filenames]
|
||||
|
||||
def __getitem__(self, i):
|
||||
result = super().__getitem__(i)
|
||||
result['inpainted'] = load_image(self.pred_filenames[i])
|
||||
if self.pad_out_to_modulo is not None and self.pad_out_to_modulo > 1:
|
||||
result['inpainted'] = pad_img_to_modulo(result['inpainted'], self.pad_out_to_modulo)
|
||||
return result
|
||||
|
||||
class OurPrecomputedInpaintingResultsDataset(OurInpaintingDataset):
|
||||
def __init__(self, datadir, predictdir, inpainted_suffix="png", **kwargs):
|
||||
super().__init__(datadir, **kwargs)
|
||||
if not datadir.endswith('/'):
|
||||
datadir += '/'
|
||||
self.predictdir = predictdir
|
||||
self.pred_filenames = [os.path.join(predictdir, os.path.basename(os.path.splitext(fname)[0]) + f'_inpainted.{inpainted_suffix}')
|
||||
for fname in self.mask_filenames]
|
||||
# self.pred_filenames = [os.path.join(predictdir, os.path.splitext(fname[len(datadir):])[0] + inpainted_suffix)
|
||||
# for fname in self.mask_filenames]
|
||||
|
||||
def __getitem__(self, i):
|
||||
result = super().__getitem__(i)
|
||||
result['inpainted'] = self.file_loader(self.pred_filenames[i])
|
||||
|
||||
if self.pad_out_to_modulo is not None and self.pad_out_to_modulo > 1:
|
||||
result['inpainted'] = pad_img_to_modulo(result['inpainted'], self.pad_out_to_modulo)
|
||||
return result
|
||||
|
||||
class InpaintingEvalOnlineDataset(Dataset):
|
||||
def __init__(self, indir, mask_generator, img_suffix='.jpg', pad_out_to_modulo=None, scale_factor=None, **kwargs):
|
||||
self.indir = indir
|
||||
self.mask_generator = mask_generator
|
||||
self.img_filenames = sorted(list(glob.glob(os.path.join(self.indir, '**', f'*{img_suffix}' ), recursive=True)))
|
||||
self.pad_out_to_modulo = pad_out_to_modulo
|
||||
self.scale_factor = scale_factor
|
||||
|
||||
def __len__(self):
|
||||
return len(self.img_filenames)
|
||||
|
||||
def __getitem__(self, i):
|
||||
img, raw_image = load_image(self.img_filenames[i], mode='RGB', return_orig=True)
|
||||
mask = self.mask_generator(img, raw_image=raw_image)
|
||||
result = dict(image=img, mask=mask)
|
||||
|
||||
if self.scale_factor is not None:
|
||||
result['image'] = scale_image(result['image'], self.scale_factor)
|
||||
result['mask'] = scale_image(result['mask'], self.scale_factor, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
if self.pad_out_to_modulo is not None and self.pad_out_to_modulo > 1:
|
||||
result['image'] = pad_img_to_modulo(result['image'], self.pad_out_to_modulo)
|
||||
result['mask'] = pad_img_to_modulo(result['mask'], self.pad_out_to_modulo)
|
||||
return result
|
||||
@@ -0,0 +1,220 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import tqdm
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
from saicinpainting.evaluation.utils import move_to_device
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InpaintingEvaluator():
|
||||
def __init__(self, dataset, scores, area_grouping=True, bins=10, batch_size=32, device='cuda',
|
||||
integral_func=None, integral_title=None, clamp_image_range=None):
|
||||
"""
|
||||
:param dataset: torch.utils.data.Dataset which contains images and masks
|
||||
:param scores: dict {score_name: EvaluatorScore object}
|
||||
:param area_grouping: in addition to the overall scores, allows to compute score for the groups of samples
|
||||
which are defined by share of area occluded by mask
|
||||
:param bins: number of groups, partition is generated by np.linspace(0., 1., bins + 1)
|
||||
:param batch_size: batch_size for the dataloader
|
||||
:param device: device to use
|
||||
"""
|
||||
self.scores = scores
|
||||
self.dataset = dataset
|
||||
|
||||
self.area_grouping = area_grouping
|
||||
self.bins = bins
|
||||
|
||||
self.device = torch.device(device)
|
||||
|
||||
self.dataloader = DataLoader(self.dataset, shuffle=False, batch_size=batch_size)
|
||||
|
||||
self.integral_func = integral_func
|
||||
self.integral_title = integral_title
|
||||
self.clamp_image_range = clamp_image_range
|
||||
|
||||
def _get_bin_edges(self):
|
||||
bin_edges = np.linspace(0, 1, self.bins + 1)
|
||||
|
||||
num_digits = max(0, math.ceil(math.log10(self.bins)) - 1)
|
||||
interval_names = []
|
||||
for idx_bin in range(self.bins):
|
||||
start_percent, end_percent = round(100 * bin_edges[idx_bin], num_digits), \
|
||||
round(100 * bin_edges[idx_bin + 1], num_digits)
|
||||
start_percent = '{:.{n}f}'.format(start_percent, n=num_digits)
|
||||
end_percent = '{:.{n}f}'.format(end_percent, n=num_digits)
|
||||
interval_names.append("{0}-{1}%".format(start_percent, end_percent))
|
||||
|
||||
groups = []
|
||||
for batch in self.dataloader:
|
||||
mask = batch['mask']
|
||||
batch_size = mask.shape[0]
|
||||
area = mask.to(self.device).reshape(batch_size, -1).mean(dim=-1)
|
||||
bin_indices = np.searchsorted(bin_edges, area.detach().cpu().numpy(), side='right') - 1
|
||||
# corner case: when area is equal to 1, bin_indices should return bins - 1, not bins for that element
|
||||
bin_indices[bin_indices == self.bins] = self.bins - 1
|
||||
groups.append(bin_indices)
|
||||
groups = np.hstack(groups)
|
||||
|
||||
return groups, interval_names
|
||||
|
||||
def evaluate(self, model=None):
|
||||
"""
|
||||
:param model: callable with signature (image_batch, mask_batch); should return inpainted_batch
|
||||
:return: dict with (score_name, group_type) as keys, where group_type can be either 'overall' or
|
||||
name of the particular group arranged by area of mask (e.g. '10-20%')
|
||||
and score statistics for the group as values.
|
||||
"""
|
||||
results = dict()
|
||||
if self.area_grouping:
|
||||
groups, interval_names = self._get_bin_edges()
|
||||
else:
|
||||
groups = None
|
||||
|
||||
for score_name, score in tqdm.auto.tqdm(self.scores.items(), desc='scores'):
|
||||
score.to(self.device)
|
||||
with torch.no_grad():
|
||||
score.reset()
|
||||
for batch in tqdm.auto.tqdm(self.dataloader, desc=score_name, leave=False):
|
||||
batch = move_to_device(batch, self.device)
|
||||
image_batch, mask_batch = batch['image'], batch['mask']
|
||||
if self.clamp_image_range is not None:
|
||||
image_batch = torch.clamp(image_batch,
|
||||
min=self.clamp_image_range[0],
|
||||
max=self.clamp_image_range[1])
|
||||
if model is None:
|
||||
assert 'inpainted' in batch, \
|
||||
'Model is None, so we expected precomputed inpainting results at key "inpainted"'
|
||||
inpainted_batch = batch['inpainted']
|
||||
else:
|
||||
inpainted_batch = model(image_batch, mask_batch)
|
||||
score(inpainted_batch, image_batch, mask_batch)
|
||||
total_results, group_results = score.get_value(groups=groups)
|
||||
|
||||
results[(score_name, 'total')] = total_results
|
||||
if groups is not None:
|
||||
for group_index, group_values in group_results.items():
|
||||
group_name = interval_names[group_index]
|
||||
results[(score_name, group_name)] = group_values
|
||||
|
||||
if self.integral_func is not None:
|
||||
results[(self.integral_title, 'total')] = dict(mean=self.integral_func(results))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def ssim_fid100_f1(metrics, fid_scale=100):
|
||||
ssim = metrics[('ssim', 'total')]['mean']
|
||||
fid = metrics[('fid', 'total')]['mean']
|
||||
fid_rel = max(0, fid_scale - fid) / fid_scale
|
||||
f1 = 2 * ssim * fid_rel / (ssim + fid_rel + 1e-3)
|
||||
return f1
|
||||
|
||||
|
||||
def lpips_fid100_f1(metrics, fid_scale=100):
|
||||
neg_lpips = 1 - metrics[('lpips', 'total')]['mean'] # invert, so bigger is better
|
||||
fid = metrics[('fid', 'total')]['mean']
|
||||
fid_rel = max(0, fid_scale - fid) / fid_scale
|
||||
f1 = 2 * neg_lpips * fid_rel / (neg_lpips + fid_rel + 1e-3)
|
||||
return f1
|
||||
|
||||
|
||||
|
||||
class InpaintingEvaluatorOnline(nn.Module):
|
||||
def __init__(self, scores, bins=10, image_key='image', inpainted_key='inpainted',
|
||||
integral_func=None, integral_title=None, clamp_image_range=None):
|
||||
"""
|
||||
:param scores: dict {score_name: EvaluatorScore object}
|
||||
:param bins: number of groups, partition is generated by np.linspace(0., 1., bins + 1)
|
||||
:param device: device to use
|
||||
"""
|
||||
super().__init__()
|
||||
LOGGER.info(f'{type(self)} init called')
|
||||
self.scores = nn.ModuleDict(scores)
|
||||
self.image_key = image_key
|
||||
self.inpainted_key = inpainted_key
|
||||
self.bins_num = bins
|
||||
self.bin_edges = np.linspace(0, 1, self.bins_num + 1)
|
||||
|
||||
num_digits = max(0, math.ceil(math.log10(self.bins_num)) - 1)
|
||||
self.interval_names = []
|
||||
for idx_bin in range(self.bins_num):
|
||||
start_percent, end_percent = round(100 * self.bin_edges[idx_bin], num_digits), \
|
||||
round(100 * self.bin_edges[idx_bin + 1], num_digits)
|
||||
start_percent = '{:.{n}f}'.format(start_percent, n=num_digits)
|
||||
end_percent = '{:.{n}f}'.format(end_percent, n=num_digits)
|
||||
self.interval_names.append("{0}-{1}%".format(start_percent, end_percent))
|
||||
|
||||
self.groups = []
|
||||
|
||||
self.integral_func = integral_func
|
||||
self.integral_title = integral_title
|
||||
self.clamp_image_range = clamp_image_range
|
||||
|
||||
LOGGER.info(f'{type(self)} init done')
|
||||
|
||||
def _get_bins(self, mask_batch):
|
||||
batch_size = mask_batch.shape[0]
|
||||
area = mask_batch.view(batch_size, -1).mean(dim=-1).detach().cpu().numpy()
|
||||
bin_indices = np.clip(np.searchsorted(self.bin_edges, area) - 1, 0, self.bins_num - 1)
|
||||
return bin_indices
|
||||
|
||||
def forward(self, batch: Dict[str, torch.Tensor]):
|
||||
"""
|
||||
Calculate and accumulate metrics for batch. To finalize evaluation and obtain final metrics, call evaluation_end
|
||||
:param batch: batch dict with mandatory fields mask, image, inpainted (can be overriden by self.inpainted_key)
|
||||
"""
|
||||
result = {}
|
||||
with torch.no_grad():
|
||||
image_batch, mask_batch, inpainted_batch = batch[self.image_key], batch['mask'], batch[self.inpainted_key]
|
||||
if self.clamp_image_range is not None:
|
||||
image_batch = torch.clamp(image_batch,
|
||||
min=self.clamp_image_range[0],
|
||||
max=self.clamp_image_range[1])
|
||||
self.groups.extend(self._get_bins(mask_batch))
|
||||
|
||||
for score_name, score in self.scores.items():
|
||||
result[score_name] = score(inpainted_batch, image_batch, mask_batch)
|
||||
return result
|
||||
|
||||
def process_batch(self, batch: Dict[str, torch.Tensor]):
|
||||
return self(batch)
|
||||
|
||||
def evaluation_end(self, states=None):
|
||||
""":return: dict with (score_name, group_type) as keys, where group_type can be either 'overall' or
|
||||
name of the particular group arranged by area of mask (e.g. '10-20%')
|
||||
and score statistics for the group as values.
|
||||
"""
|
||||
LOGGER.info(f'{type(self)}: evaluation_end called')
|
||||
|
||||
self.groups = np.array(self.groups)
|
||||
|
||||
results = {}
|
||||
for score_name, score in self.scores.items():
|
||||
LOGGER.info(f'Getting value of {score_name}')
|
||||
cur_states = [s[score_name] for s in states] if states is not None else None
|
||||
total_results, group_results = score.get_value(groups=self.groups, states=cur_states)
|
||||
LOGGER.info(f'Getting value of {score_name} done')
|
||||
results[(score_name, 'total')] = total_results
|
||||
|
||||
for group_index, group_values in group_results.items():
|
||||
group_name = self.interval_names[group_index]
|
||||
results[(score_name, group_name)] = group_values
|
||||
|
||||
if self.integral_func is not None:
|
||||
results[(self.integral_title, 'total')] = dict(mean=self.integral_func(results))
|
||||
|
||||
LOGGER.info(f'{type(self)}: reset scores')
|
||||
self.groups = []
|
||||
for sc in self.scores.values():
|
||||
sc.reset()
|
||||
LOGGER.info(f'{type(self)}: reset scores done')
|
||||
|
||||
LOGGER.info(f'{type(self)}: evaluation_end done')
|
||||
return results
|
||||
@@ -0,0 +1,528 @@
|
||||
import logging
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
import numpy as np
|
||||
import sklearn
|
||||
import sklearn.svm
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from joblib import Parallel, delayed
|
||||
from scipy import linalg
|
||||
|
||||
from models.ade20k import SegmentationModule, NUM_CLASS, segm_options
|
||||
from .fid.inception import InceptionV3
|
||||
from .lpips import PerceptualLoss
|
||||
from .ssim import SSIM
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_groupings(groups):
|
||||
"""
|
||||
:param groups: group numbers for respective elements
|
||||
:return: dict of kind {group_idx: indices of the corresponding group elements}
|
||||
"""
|
||||
label_groups, count_groups = np.unique(groups, return_counts=True)
|
||||
|
||||
indices = np.argsort(groups)
|
||||
|
||||
grouping = dict()
|
||||
cur_start = 0
|
||||
for label, count in zip(label_groups, count_groups):
|
||||
cur_end = cur_start + count
|
||||
cur_indices = indices[cur_start:cur_end]
|
||||
grouping[label] = cur_indices
|
||||
cur_start = cur_end
|
||||
return grouping
|
||||
|
||||
|
||||
class EvaluatorScore(nn.Module):
|
||||
@abstractmethod
|
||||
def forward(self, pred_batch, target_batch, mask):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_value(self, groups=None, states=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
|
||||
class PairwiseScore(EvaluatorScore, ABC):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.individual_values = None
|
||||
|
||||
def get_value(self, groups=None, states=None):
|
||||
"""
|
||||
:param groups:
|
||||
:return:
|
||||
total_results: dict of kind {'mean': score mean, 'std': score std}
|
||||
group_results: None, if groups is None;
|
||||
else dict {group_idx: {'mean': score mean among group, 'std': score std among group}}
|
||||
"""
|
||||
individual_values = torch.cat(states, dim=-1).reshape(-1).cpu().numpy() if states is not None \
|
||||
else self.individual_values
|
||||
|
||||
total_results = {
|
||||
'mean': individual_values.mean(),
|
||||
'std': individual_values.std()
|
||||
}
|
||||
|
||||
if groups is None:
|
||||
return total_results, None
|
||||
|
||||
group_results = dict()
|
||||
grouping = get_groupings(groups)
|
||||
for label, index in grouping.items():
|
||||
group_scores = individual_values[index]
|
||||
group_results[label] = {
|
||||
'mean': group_scores.mean(),
|
||||
'std': group_scores.std()
|
||||
}
|
||||
return total_results, group_results
|
||||
|
||||
def reset(self):
|
||||
self.individual_values = []
|
||||
|
||||
|
||||
class SSIMScore(PairwiseScore):
|
||||
def __init__(self, window_size=11):
|
||||
super().__init__()
|
||||
self.score = SSIM(window_size=window_size, size_average=False).eval()
|
||||
self.reset()
|
||||
|
||||
def forward(self, pred_batch, target_batch, mask=None):
|
||||
batch_values = self.score(pred_batch, target_batch)
|
||||
self.individual_values = np.hstack([
|
||||
self.individual_values, batch_values.detach().cpu().numpy()
|
||||
])
|
||||
return batch_values
|
||||
|
||||
|
||||
class LPIPSScore(PairwiseScore):
|
||||
def __init__(self, model='net-lin', net='vgg', model_path=None, use_gpu=True):
|
||||
super().__init__()
|
||||
self.score = PerceptualLoss(model=model, net=net, model_path=model_path,
|
||||
use_gpu=use_gpu, spatial=False).eval()
|
||||
self.reset()
|
||||
|
||||
def forward(self, pred_batch, target_batch, mask=None):
|
||||
batch_values = self.score(pred_batch, target_batch).flatten()
|
||||
self.individual_values = np.hstack([
|
||||
self.individual_values, batch_values.detach().cpu().numpy()
|
||||
])
|
||||
return batch_values
|
||||
|
||||
|
||||
def fid_calculate_activation_statistics(act):
|
||||
mu = np.mean(act, axis=0)
|
||||
sigma = np.cov(act, rowvar=False)
|
||||
return mu, sigma
|
||||
|
||||
|
||||
def calculate_frechet_distance(activations_pred, activations_target, eps=1e-6):
|
||||
mu1, sigma1 = fid_calculate_activation_statistics(activations_pred)
|
||||
mu2, sigma2 = fid_calculate_activation_statistics(activations_target)
|
||||
|
||||
diff = mu1 - mu2
|
||||
|
||||
# Product might be almost singular
|
||||
covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)
|
||||
if not np.isfinite(covmean).all():
|
||||
msg = ('fid calculation produces singular product; '
|
||||
'adding %s to diagonal of cov estimates') % eps
|
||||
LOGGER.warning(msg)
|
||||
offset = np.eye(sigma1.shape[0]) * eps
|
||||
covmean = linalg.sqrtm((sigma1 + offset).dot(sigma2 + offset))
|
||||
|
||||
# Numerical error might give slight imaginary component
|
||||
if np.iscomplexobj(covmean):
|
||||
# if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-3):
|
||||
if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-2):
|
||||
m = np.max(np.abs(covmean.imag))
|
||||
raise ValueError('Imaginary component {}'.format(m))
|
||||
covmean = covmean.real
|
||||
|
||||
tr_covmean = np.trace(covmean)
|
||||
|
||||
return (diff.dot(diff) + np.trace(sigma1) +
|
||||
np.trace(sigma2) - 2 * tr_covmean)
|
||||
|
||||
|
||||
class FIDScore(EvaluatorScore):
|
||||
def __init__(self, dims=2048, eps=1e-6):
|
||||
LOGGER.info("FIDscore init called")
|
||||
super().__init__()
|
||||
if getattr(FIDScore, '_MODEL', None) is None:
|
||||
block_idx = InceptionV3.BLOCK_INDEX_BY_DIM[dims]
|
||||
FIDScore._MODEL = InceptionV3([block_idx]).eval()
|
||||
self.model = FIDScore._MODEL
|
||||
self.eps = eps
|
||||
self.reset()
|
||||
LOGGER.info("FIDscore init done")
|
||||
|
||||
def forward(self, pred_batch, target_batch, mask=None):
|
||||
activations_pred = self._get_activations(pred_batch)
|
||||
activations_target = self._get_activations(target_batch)
|
||||
|
||||
self.activations_pred.append(activations_pred.detach().cpu())
|
||||
self.activations_target.append(activations_target.detach().cpu())
|
||||
|
||||
return activations_pred, activations_target
|
||||
|
||||
def get_value(self, groups=None, states=None):
|
||||
LOGGER.info("FIDscore get_value called")
|
||||
activations_pred, activations_target = zip(*states) if states is not None \
|
||||
else (self.activations_pred, self.activations_target)
|
||||
activations_pred = torch.cat(activations_pred).cpu().numpy()
|
||||
activations_target = torch.cat(activations_target).cpu().numpy()
|
||||
|
||||
total_distance = calculate_frechet_distance(activations_pred, activations_target, eps=self.eps)
|
||||
total_results = dict(mean=total_distance)
|
||||
|
||||
if groups is None:
|
||||
group_results = None
|
||||
else:
|
||||
group_results = dict()
|
||||
grouping = get_groupings(groups)
|
||||
for label, index in grouping.items():
|
||||
if len(index) > 1:
|
||||
group_distance = calculate_frechet_distance(activations_pred[index], activations_target[index],
|
||||
eps=self.eps)
|
||||
group_results[label] = dict(mean=group_distance)
|
||||
|
||||
else:
|
||||
group_results[label] = dict(mean=float('nan'))
|
||||
|
||||
self.reset()
|
||||
|
||||
LOGGER.info("FIDscore get_value done")
|
||||
|
||||
return total_results, group_results
|
||||
|
||||
def reset(self):
|
||||
self.activations_pred = []
|
||||
self.activations_target = []
|
||||
|
||||
def _get_activations(self, batch):
|
||||
activations = self.model(batch)[0]
|
||||
if activations.shape[2] != 1 or activations.shape[3] != 1:
|
||||
assert False, \
|
||||
'We should not have got here, because Inception always scales inputs to 299x299'
|
||||
# activations = F.adaptive_avg_pool2d(activations, output_size=(1, 1))
|
||||
activations = activations.squeeze(-1).squeeze(-1)
|
||||
return activations
|
||||
|
||||
|
||||
class SegmentationAwareScore(EvaluatorScore):
|
||||
def __init__(self, weights_path):
|
||||
super().__init__()
|
||||
self.segm_network = SegmentationModule(weights_path=weights_path, use_default_normalization=True).eval()
|
||||
self.target_class_freq_by_image_total = []
|
||||
self.target_class_freq_by_image_mask = []
|
||||
self.pred_class_freq_by_image_mask = []
|
||||
|
||||
def forward(self, pred_batch, target_batch, mask):
|
||||
pred_segm_flat = self.segm_network.predict(pred_batch)[0].view(pred_batch.shape[0], -1).long().detach().cpu().numpy()
|
||||
target_segm_flat = self.segm_network.predict(target_batch)[0].view(pred_batch.shape[0], -1).long().detach().cpu().numpy()
|
||||
mask_flat = (mask.view(mask.shape[0], -1) > 0.5).detach().cpu().numpy()
|
||||
|
||||
batch_target_class_freq_total = []
|
||||
batch_target_class_freq_mask = []
|
||||
batch_pred_class_freq_mask = []
|
||||
|
||||
for cur_pred_segm, cur_target_segm, cur_mask in zip(pred_segm_flat, target_segm_flat, mask_flat):
|
||||
cur_target_class_freq_total = np.bincount(cur_target_segm, minlength=NUM_CLASS)[None, ...]
|
||||
cur_target_class_freq_mask = np.bincount(cur_target_segm[cur_mask], minlength=NUM_CLASS)[None, ...]
|
||||
cur_pred_class_freq_mask = np.bincount(cur_pred_segm[cur_mask], minlength=NUM_CLASS)[None, ...]
|
||||
|
||||
self.target_class_freq_by_image_total.append(cur_target_class_freq_total)
|
||||
self.target_class_freq_by_image_mask.append(cur_target_class_freq_mask)
|
||||
self.pred_class_freq_by_image_mask.append(cur_pred_class_freq_mask)
|
||||
|
||||
batch_target_class_freq_total.append(cur_target_class_freq_total)
|
||||
batch_target_class_freq_mask.append(cur_target_class_freq_mask)
|
||||
batch_pred_class_freq_mask.append(cur_pred_class_freq_mask)
|
||||
|
||||
batch_target_class_freq_total = np.concatenate(batch_target_class_freq_total, axis=0)
|
||||
batch_target_class_freq_mask = np.concatenate(batch_target_class_freq_mask, axis=0)
|
||||
batch_pred_class_freq_mask = np.concatenate(batch_pred_class_freq_mask, axis=0)
|
||||
return batch_target_class_freq_total, batch_target_class_freq_mask, batch_pred_class_freq_mask
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
self.target_class_freq_by_image_total = []
|
||||
self.target_class_freq_by_image_mask = []
|
||||
self.pred_class_freq_by_image_mask = []
|
||||
|
||||
|
||||
def distribute_values_to_classes(target_class_freq_by_image_mask, values, idx2name):
|
||||
assert target_class_freq_by_image_mask.ndim == 2 and target_class_freq_by_image_mask.shape[0] == values.shape[0]
|
||||
total_class_freq = target_class_freq_by_image_mask.sum(0)
|
||||
distr_values = (target_class_freq_by_image_mask * values[..., None]).sum(0)
|
||||
result = distr_values / (total_class_freq + 1e-3)
|
||||
return {idx2name[i]: val for i, val in enumerate(result) if total_class_freq[i] > 0}
|
||||
|
||||
|
||||
def get_segmentation_idx2name():
|
||||
return {i - 1: name for i, name in segm_options['classes'].set_index('Idx', drop=True)['Name'].to_dict().items()}
|
||||
|
||||
|
||||
class SegmentationAwarePairwiseScore(SegmentationAwareScore):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.individual_values = []
|
||||
self.segm_idx2name = get_segmentation_idx2name()
|
||||
|
||||
def forward(self, pred_batch, target_batch, mask):
|
||||
cur_class_stats = super().forward(pred_batch, target_batch, mask)
|
||||
score_values = self.calc_score(pred_batch, target_batch, mask)
|
||||
self.individual_values.append(score_values)
|
||||
return cur_class_stats + (score_values,)
|
||||
|
||||
@abstractmethod
|
||||
def calc_score(self, pred_batch, target_batch, mask):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_value(self, groups=None, states=None):
|
||||
"""
|
||||
:param groups:
|
||||
:return:
|
||||
total_results: dict of kind {'mean': score mean, 'std': score std}
|
||||
group_results: None, if groups is None;
|
||||
else dict {group_idx: {'mean': score mean among group, 'std': score std among group}}
|
||||
"""
|
||||
if states is not None:
|
||||
(target_class_freq_by_image_total,
|
||||
target_class_freq_by_image_mask,
|
||||
pred_class_freq_by_image_mask,
|
||||
individual_values) = states
|
||||
else:
|
||||
target_class_freq_by_image_total = self.target_class_freq_by_image_total
|
||||
target_class_freq_by_image_mask = self.target_class_freq_by_image_mask
|
||||
pred_class_freq_by_image_mask = self.pred_class_freq_by_image_mask
|
||||
individual_values = self.individual_values
|
||||
|
||||
target_class_freq_by_image_total = np.concatenate(target_class_freq_by_image_total, axis=0)
|
||||
target_class_freq_by_image_mask = np.concatenate(target_class_freq_by_image_mask, axis=0)
|
||||
pred_class_freq_by_image_mask = np.concatenate(pred_class_freq_by_image_mask, axis=0)
|
||||
individual_values = np.concatenate(individual_values, axis=0)
|
||||
|
||||
total_results = {
|
||||
'mean': individual_values.mean(),
|
||||
'std': individual_values.std(),
|
||||
**distribute_values_to_classes(target_class_freq_by_image_mask, individual_values, self.segm_idx2name)
|
||||
}
|
||||
|
||||
if groups is None:
|
||||
return total_results, None
|
||||
|
||||
group_results = dict()
|
||||
grouping = get_groupings(groups)
|
||||
for label, index in grouping.items():
|
||||
group_class_freq = target_class_freq_by_image_mask[index]
|
||||
group_scores = individual_values[index]
|
||||
group_results[label] = {
|
||||
'mean': group_scores.mean(),
|
||||
'std': group_scores.std(),
|
||||
** distribute_values_to_classes(group_class_freq, group_scores, self.segm_idx2name)
|
||||
}
|
||||
return total_results, group_results
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
self.individual_values = []
|
||||
|
||||
|
||||
class SegmentationClassStats(SegmentationAwarePairwiseScore):
|
||||
def calc_score(self, pred_batch, target_batch, mask):
|
||||
return 0
|
||||
|
||||
def get_value(self, groups=None, states=None):
|
||||
"""
|
||||
:param groups:
|
||||
:return:
|
||||
total_results: dict of kind {'mean': score mean, 'std': score std}
|
||||
group_results: None, if groups is None;
|
||||
else dict {group_idx: {'mean': score mean among group, 'std': score std among group}}
|
||||
"""
|
||||
if states is not None:
|
||||
(target_class_freq_by_image_total,
|
||||
target_class_freq_by_image_mask,
|
||||
pred_class_freq_by_image_mask,
|
||||
_) = states
|
||||
else:
|
||||
target_class_freq_by_image_total = self.target_class_freq_by_image_total
|
||||
target_class_freq_by_image_mask = self.target_class_freq_by_image_mask
|
||||
pred_class_freq_by_image_mask = self.pred_class_freq_by_image_mask
|
||||
|
||||
target_class_freq_by_image_total = np.concatenate(target_class_freq_by_image_total, axis=0)
|
||||
target_class_freq_by_image_mask = np.concatenate(target_class_freq_by_image_mask, axis=0)
|
||||
pred_class_freq_by_image_mask = np.concatenate(pred_class_freq_by_image_mask, axis=0)
|
||||
|
||||
target_class_freq_by_image_total_marginal = target_class_freq_by_image_total.sum(0).astype('float32')
|
||||
target_class_freq_by_image_total_marginal /= target_class_freq_by_image_total_marginal.sum()
|
||||
|
||||
target_class_freq_by_image_mask_marginal = target_class_freq_by_image_mask.sum(0).astype('float32')
|
||||
target_class_freq_by_image_mask_marginal /= target_class_freq_by_image_mask_marginal.sum()
|
||||
|
||||
pred_class_freq_diff = (pred_class_freq_by_image_mask - target_class_freq_by_image_mask).sum(0) / (target_class_freq_by_image_mask.sum(0) + 1e-3)
|
||||
|
||||
total_results = dict()
|
||||
total_results.update({f'total_freq/{self.segm_idx2name[i]}': v
|
||||
for i, v in enumerate(target_class_freq_by_image_total_marginal)
|
||||
if v > 0})
|
||||
total_results.update({f'mask_freq/{self.segm_idx2name[i]}': v
|
||||
for i, v in enumerate(target_class_freq_by_image_mask_marginal)
|
||||
if v > 0})
|
||||
total_results.update({f'mask_freq_diff/{self.segm_idx2name[i]}': v
|
||||
for i, v in enumerate(pred_class_freq_diff)
|
||||
if target_class_freq_by_image_total_marginal[i] > 0})
|
||||
|
||||
if groups is None:
|
||||
return total_results, None
|
||||
|
||||
group_results = dict()
|
||||
grouping = get_groupings(groups)
|
||||
for label, index in grouping.items():
|
||||
group_target_class_freq_by_image_total = target_class_freq_by_image_total[index]
|
||||
group_target_class_freq_by_image_mask = target_class_freq_by_image_mask[index]
|
||||
group_pred_class_freq_by_image_mask = pred_class_freq_by_image_mask[index]
|
||||
|
||||
group_target_class_freq_by_image_total_marginal = group_target_class_freq_by_image_total.sum(0).astype('float32')
|
||||
group_target_class_freq_by_image_total_marginal /= group_target_class_freq_by_image_total_marginal.sum()
|
||||
|
||||
group_target_class_freq_by_image_mask_marginal = group_target_class_freq_by_image_mask.sum(0).astype('float32')
|
||||
group_target_class_freq_by_image_mask_marginal /= group_target_class_freq_by_image_mask_marginal.sum()
|
||||
|
||||
group_pred_class_freq_diff = (group_pred_class_freq_by_image_mask - group_target_class_freq_by_image_mask).sum(0) / (
|
||||
group_target_class_freq_by_image_mask.sum(0) + 1e-3)
|
||||
|
||||
cur_group_results = dict()
|
||||
cur_group_results.update({f'total_freq/{self.segm_idx2name[i]}': v
|
||||
for i, v in enumerate(group_target_class_freq_by_image_total_marginal)
|
||||
if v > 0})
|
||||
cur_group_results.update({f'mask_freq/{self.segm_idx2name[i]}': v
|
||||
for i, v in enumerate(group_target_class_freq_by_image_mask_marginal)
|
||||
if v > 0})
|
||||
cur_group_results.update({f'mask_freq_diff/{self.segm_idx2name[i]}': v
|
||||
for i, v in enumerate(group_pred_class_freq_diff)
|
||||
if group_target_class_freq_by_image_total_marginal[i] > 0})
|
||||
|
||||
group_results[label] = cur_group_results
|
||||
return total_results, group_results
|
||||
|
||||
|
||||
class SegmentationAwareSSIM(SegmentationAwarePairwiseScore):
|
||||
def __init__(self, *args, window_size=11, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.score_impl = SSIM(window_size=window_size, size_average=False).eval()
|
||||
|
||||
def calc_score(self, pred_batch, target_batch, mask):
|
||||
return self.score_impl(pred_batch, target_batch).detach().cpu().numpy()
|
||||
|
||||
|
||||
class SegmentationAwareLPIPS(SegmentationAwarePairwiseScore):
|
||||
def __init__(self, *args, model='net-lin', net='vgg', model_path=None, use_gpu=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.score_impl = PerceptualLoss(model=model, net=net, model_path=model_path,
|
||||
use_gpu=use_gpu, spatial=False).eval()
|
||||
|
||||
def calc_score(self, pred_batch, target_batch, mask):
|
||||
return self.score_impl(pred_batch, target_batch).flatten().detach().cpu().numpy()
|
||||
|
||||
|
||||
def calculade_fid_no_img(img_i, activations_pred, activations_target, eps=1e-6):
|
||||
activations_pred = activations_pred.copy()
|
||||
activations_pred[img_i] = activations_target[img_i]
|
||||
return calculate_frechet_distance(activations_pred, activations_target, eps=eps)
|
||||
|
||||
|
||||
class SegmentationAwareFID(SegmentationAwarePairwiseScore):
|
||||
def __init__(self, *args, dims=2048, eps=1e-6, n_jobs=-1, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if getattr(FIDScore, '_MODEL', None) is None:
|
||||
block_idx = InceptionV3.BLOCK_INDEX_BY_DIM[dims]
|
||||
FIDScore._MODEL = InceptionV3([block_idx]).eval()
|
||||
self.model = FIDScore._MODEL
|
||||
self.eps = eps
|
||||
self.n_jobs = n_jobs
|
||||
|
||||
def calc_score(self, pred_batch, target_batch, mask):
|
||||
activations_pred = self._get_activations(pred_batch)
|
||||
activations_target = self._get_activations(target_batch)
|
||||
return activations_pred, activations_target
|
||||
|
||||
def get_value(self, groups=None, states=None):
|
||||
"""
|
||||
:param groups:
|
||||
:return:
|
||||
total_results: dict of kind {'mean': score mean, 'std': score std}
|
||||
group_results: None, if groups is None;
|
||||
else dict {group_idx: {'mean': score mean among group, 'std': score std among group}}
|
||||
"""
|
||||
if states is not None:
|
||||
(target_class_freq_by_image_total,
|
||||
target_class_freq_by_image_mask,
|
||||
pred_class_freq_by_image_mask,
|
||||
activation_pairs) = states
|
||||
else:
|
||||
target_class_freq_by_image_total = self.target_class_freq_by_image_total
|
||||
target_class_freq_by_image_mask = self.target_class_freq_by_image_mask
|
||||
pred_class_freq_by_image_mask = self.pred_class_freq_by_image_mask
|
||||
activation_pairs = self.individual_values
|
||||
|
||||
target_class_freq_by_image_total = np.concatenate(target_class_freq_by_image_total, axis=0)
|
||||
target_class_freq_by_image_mask = np.concatenate(target_class_freq_by_image_mask, axis=0)
|
||||
pred_class_freq_by_image_mask = np.concatenate(pred_class_freq_by_image_mask, axis=0)
|
||||
activations_pred, activations_target = zip(*activation_pairs)
|
||||
activations_pred = np.concatenate(activations_pred, axis=0)
|
||||
activations_target = np.concatenate(activations_target, axis=0)
|
||||
|
||||
total_results = {
|
||||
'mean': calculate_frechet_distance(activations_pred, activations_target, eps=self.eps),
|
||||
'std': 0,
|
||||
**self.distribute_fid_to_classes(target_class_freq_by_image_mask, activations_pred, activations_target)
|
||||
}
|
||||
|
||||
if groups is None:
|
||||
return total_results, None
|
||||
|
||||
group_results = dict()
|
||||
grouping = get_groupings(groups)
|
||||
for label, index in grouping.items():
|
||||
if len(index) > 1:
|
||||
group_activations_pred = activations_pred[index]
|
||||
group_activations_target = activations_target[index]
|
||||
group_class_freq = target_class_freq_by_image_mask[index]
|
||||
group_results[label] = {
|
||||
'mean': calculate_frechet_distance(group_activations_pred, group_activations_target, eps=self.eps),
|
||||
'std': 0,
|
||||
**self.distribute_fid_to_classes(group_class_freq,
|
||||
group_activations_pred,
|
||||
group_activations_target)
|
||||
}
|
||||
else:
|
||||
group_results[label] = dict(mean=float('nan'), std=0)
|
||||
return total_results, group_results
|
||||
|
||||
def distribute_fid_to_classes(self, class_freq, activations_pred, activations_target):
|
||||
real_fid = calculate_frechet_distance(activations_pred, activations_target, eps=self.eps)
|
||||
|
||||
fid_no_images = Parallel(n_jobs=self.n_jobs)(
|
||||
delayed(calculade_fid_no_img)(img_i, activations_pred, activations_target, eps=self.eps)
|
||||
for img_i in range(activations_pred.shape[0])
|
||||
)
|
||||
errors = real_fid - fid_no_images
|
||||
return distribute_values_to_classes(class_freq, errors, self.segm_idx2name)
|
||||
|
||||
def _get_activations(self, batch):
|
||||
activations = self.model(batch)[0]
|
||||
if activations.shape[2] != 1 or activations.shape[3] != 1:
|
||||
activations = F.adaptive_avg_pool2d(activations, output_size=(1, 1))
|
||||
activations = activations.squeeze(-1).squeeze(-1).detach().cpu().numpy()
|
||||
return activations
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Calculates the Frechet Inception Distance (FID) to evalulate GANs
|
||||
|
||||
The FID metric calculates the distance between two distributions of images.
|
||||
Typically, we have summary statistics (mean & covariance matrix) of one
|
||||
of these distributions, while the 2nd distribution is given by a GAN.
|
||||
|
||||
When run as a stand-alone program, it compares the distribution of
|
||||
images that are stored as PNG/JPEG at a specified location with a
|
||||
distribution given by summary statistics (in pickle format).
|
||||
|
||||
The FID is calculated by assuming that X_1 and X_2 are the activations of
|
||||
the pool_3 layer of the inception net for generated samples and real world
|
||||
samples respectively.
|
||||
|
||||
See --help to see further details.
|
||||
|
||||
Code apapted from https://github.com/bioinf-jku/TTUR to use PyTorch instead
|
||||
of Tensorflow
|
||||
|
||||
Copyright 2018 Institute of Bioinformatics, JKU Linz
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
import os
|
||||
import pathlib
|
||||
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
# from scipy.misc import imread
|
||||
from imageio import imread
|
||||
from PIL import Image, JpegImagePlugin
|
||||
from scipy import linalg
|
||||
from torch.nn.functional import adaptive_avg_pool2d
|
||||
from torchvision.transforms import CenterCrop, Compose, Resize, ToTensor
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
# If not tqdm is not available, provide a mock version of it
|
||||
def tqdm(x): return x
|
||||
|
||||
try:
|
||||
from .inception import InceptionV3
|
||||
except ModuleNotFoundError:
|
||||
from inception import InceptionV3
|
||||
|
||||
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('path', type=str, nargs=2,
|
||||
help=('Path to the generated images or '
|
||||
'to .npz statistic files'))
|
||||
parser.add_argument('--batch-size', type=int, default=50,
|
||||
help='Batch size to use')
|
||||
parser.add_argument('--dims', type=int, default=2048,
|
||||
choices=list(InceptionV3.BLOCK_INDEX_BY_DIM),
|
||||
help=('Dimensionality of Inception features to use. '
|
||||
'By default, uses pool3 features'))
|
||||
parser.add_argument('-c', '--gpu', default='', type=str,
|
||||
help='GPU to use (leave blank for CPU only)')
|
||||
parser.add_argument('--resize', default=256)
|
||||
|
||||
transform = Compose([Resize(256), CenterCrop(256), ToTensor()])
|
||||
|
||||
|
||||
def get_activations(files, model, batch_size=50, dims=2048,
|
||||
cuda=False, verbose=False, keep_size=False):
|
||||
"""Calculates the activations of the pool_3 layer for all images.
|
||||
|
||||
Params:
|
||||
-- files : List of image files paths
|
||||
-- model : Instance of inception model
|
||||
-- batch_size : Batch size of images for the model to process at once.
|
||||
Make sure that the number of samples is a multiple of
|
||||
the batch size, otherwise some samples are ignored. This
|
||||
behavior is retained to match the original FID score
|
||||
implementation.
|
||||
-- dims : Dimensionality of features returned by Inception
|
||||
-- cuda : If set to True, use GPU
|
||||
-- verbose : If set to True and parameter out_step is given, the number
|
||||
of calculated batches is reported.
|
||||
Returns:
|
||||
-- A numpy array of dimension (num images, dims) that contains the
|
||||
activations of the given tensor when feeding inception with the
|
||||
query tensor.
|
||||
"""
|
||||
model.eval()
|
||||
|
||||
if len(files) % batch_size != 0:
|
||||
print(('Warning: number of images is not a multiple of the '
|
||||
'batch size. Some samples are going to be ignored.'))
|
||||
if batch_size > len(files):
|
||||
print(('Warning: batch size is bigger than the data size. '
|
||||
'Setting batch size to data size'))
|
||||
batch_size = len(files)
|
||||
|
||||
n_batches = len(files) // batch_size
|
||||
n_used_imgs = n_batches * batch_size
|
||||
|
||||
pred_arr = np.empty((n_used_imgs, dims))
|
||||
|
||||
for i in tqdm(range(n_batches)):
|
||||
if verbose:
|
||||
print('\rPropagating batch %d/%d' % (i + 1, n_batches),
|
||||
end='', flush=True)
|
||||
start = i * batch_size
|
||||
end = start + batch_size
|
||||
|
||||
# # Official code goes below
|
||||
# images = np.array([imread(str(f)).astype(np.float32)
|
||||
# for f in files[start:end]])
|
||||
|
||||
# # Reshape to (n_images, 3, height, width)
|
||||
# images = images.transpose((0, 3, 1, 2))
|
||||
# images /= 255
|
||||
# batch = torch.from_numpy(images).type(torch.FloatTensor)
|
||||
# #
|
||||
|
||||
t = transform if not keep_size else ToTensor()
|
||||
|
||||
if isinstance(files[0], pathlib.PosixPath):
|
||||
images = [t(Image.open(str(f))) for f in files[start:end]]
|
||||
|
||||
elif isinstance(files[0], Image.Image):
|
||||
images = [t(f) for f in files[start:end]]
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown data type for image: {type(files[0])}")
|
||||
|
||||
batch = torch.stack(images)
|
||||
|
||||
if cuda:
|
||||
batch = batch.cuda()
|
||||
|
||||
pred = model(batch)[0]
|
||||
|
||||
# If model output is not scalar, apply global spatial average pooling.
|
||||
# This happens if you choose a dimensionality not equal 2048.
|
||||
if pred.shape[2] != 1 or pred.shape[3] != 1:
|
||||
pred = adaptive_avg_pool2d(pred, output_size=(1, 1))
|
||||
|
||||
pred_arr[start:end] = pred.cpu().data.numpy().reshape(batch_size, -1)
|
||||
|
||||
if verbose:
|
||||
print(' done')
|
||||
|
||||
return pred_arr
|
||||
|
||||
|
||||
def calculate_frechet_distance(mu1, sigma1, mu2, sigma2, eps=1e-6):
|
||||
"""Numpy implementation of the Frechet Distance.
|
||||
The Frechet distance between two multivariate Gaussians X_1 ~ N(mu_1, C_1)
|
||||
and X_2 ~ N(mu_2, C_2) is
|
||||
d^2 = ||mu_1 - mu_2||^2 + Tr(C_1 + C_2 - 2*sqrt(C_1*C_2)).
|
||||
|
||||
Stable version by Dougal J. Sutherland.
|
||||
|
||||
Params:
|
||||
-- mu1 : Numpy array containing the activations of a layer of the
|
||||
inception net (like returned by the function 'get_predictions')
|
||||
for generated samples.
|
||||
-- mu2 : The sample mean over activations, precalculated on an
|
||||
representative data set.
|
||||
-- sigma1: The covariance matrix over activations for generated samples.
|
||||
-- sigma2: The covariance matrix over activations, precalculated on an
|
||||
representative data set.
|
||||
|
||||
Returns:
|
||||
-- : The Frechet Distance.
|
||||
"""
|
||||
|
||||
mu1 = np.atleast_1d(mu1)
|
||||
mu2 = np.atleast_1d(mu2)
|
||||
|
||||
sigma1 = np.atleast_2d(sigma1)
|
||||
sigma2 = np.atleast_2d(sigma2)
|
||||
|
||||
assert mu1.shape == mu2.shape, \
|
||||
'Training and test mean vectors have different lengths'
|
||||
assert sigma1.shape == sigma2.shape, \
|
||||
'Training and test covariances have different dimensions'
|
||||
|
||||
diff = mu1 - mu2
|
||||
|
||||
# Product might be almost singular
|
||||
covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)
|
||||
if not np.isfinite(covmean).all():
|
||||
msg = ('fid calculation produces singular product; '
|
||||
'adding %s to diagonal of cov estimates') % eps
|
||||
print(msg)
|
||||
offset = np.eye(sigma1.shape[0]) * eps
|
||||
covmean = linalg.sqrtm((sigma1 + offset).dot(sigma2 + offset))
|
||||
|
||||
# Numerical error might give slight imaginary component
|
||||
if np.iscomplexobj(covmean):
|
||||
# if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-3):
|
||||
if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-2):
|
||||
m = np.max(np.abs(covmean.imag))
|
||||
raise ValueError('Imaginary component {}'.format(m))
|
||||
covmean = covmean.real
|
||||
|
||||
tr_covmean = np.trace(covmean)
|
||||
|
||||
return (diff.dot(diff) + np.trace(sigma1) +
|
||||
np.trace(sigma2) - 2 * tr_covmean)
|
||||
|
||||
|
||||
def calculate_activation_statistics(files, model, batch_size=50,
|
||||
dims=2048, cuda=False, verbose=False, keep_size=False):
|
||||
"""Calculation of the statistics used by the FID.
|
||||
Params:
|
||||
-- files : List of image files paths
|
||||
-- model : Instance of inception model
|
||||
-- batch_size : The images numpy array is split into batches with
|
||||
batch size batch_size. A reasonable batch size
|
||||
depends on the hardware.
|
||||
-- dims : Dimensionality of features returned by Inception
|
||||
-- cuda : If set to True, use GPU
|
||||
-- verbose : If set to True and parameter out_step is given, the
|
||||
number of calculated batches is reported.
|
||||
Returns:
|
||||
-- mu : The mean over samples of the activations of the pool_3 layer of
|
||||
the inception model.
|
||||
-- sigma : The covariance matrix of the activations of the pool_3 layer of
|
||||
the inception model.
|
||||
"""
|
||||
act = get_activations(files, model, batch_size, dims, cuda, verbose, keep_size=keep_size)
|
||||
mu = np.mean(act, axis=0)
|
||||
sigma = np.cov(act, rowvar=False)
|
||||
return mu, sigma
|
||||
|
||||
|
||||
def _compute_statistics_of_path(path, model, batch_size, dims, cuda):
|
||||
if path.endswith('.npz'):
|
||||
f = np.load(path)
|
||||
m, s = f['mu'][:], f['sigma'][:]
|
||||
f.close()
|
||||
else:
|
||||
path = pathlib.Path(path)
|
||||
files = list(path.glob('*.jpg')) + list(path.glob('*.png'))
|
||||
m, s = calculate_activation_statistics(files, model, batch_size,
|
||||
dims, cuda)
|
||||
|
||||
return m, s
|
||||
|
||||
|
||||
def _compute_statistics_of_images(images, model, batch_size, dims, cuda, keep_size=False):
|
||||
if isinstance(images, list): # exact paths to files are provided
|
||||
m, s = calculate_activation_statistics(images, model, batch_size,
|
||||
dims, cuda, keep_size=keep_size)
|
||||
|
||||
return m, s
|
||||
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
|
||||
def calculate_fid_given_paths(paths, batch_size, cuda, dims):
|
||||
"""Calculates the FID of two paths"""
|
||||
for p in paths:
|
||||
if not os.path.exists(p):
|
||||
raise RuntimeError('Invalid path: %s' % p)
|
||||
|
||||
block_idx = InceptionV3.BLOCK_INDEX_BY_DIM[dims]
|
||||
|
||||
model = InceptionV3([block_idx])
|
||||
if cuda:
|
||||
model.cuda()
|
||||
|
||||
m1, s1 = _compute_statistics_of_path(paths[0], model, batch_size,
|
||||
dims, cuda)
|
||||
m2, s2 = _compute_statistics_of_path(paths[1], model, batch_size,
|
||||
dims, cuda)
|
||||
fid_value = calculate_frechet_distance(m1, s1, m2, s2)
|
||||
|
||||
return fid_value
|
||||
|
||||
|
||||
def calculate_fid_given_images(images, batch_size, cuda, dims, use_globals=False, keep_size=False):
|
||||
if use_globals:
|
||||
global FID_MODEL # for multiprocessing
|
||||
|
||||
for imgs in images:
|
||||
if isinstance(imgs, list) and isinstance(imgs[0], (Image.Image, JpegImagePlugin.JpegImageFile)):
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError('Invalid images')
|
||||
|
||||
block_idx = InceptionV3.BLOCK_INDEX_BY_DIM[dims]
|
||||
|
||||
if 'FID_MODEL' not in globals() or not use_globals:
|
||||
model = InceptionV3([block_idx])
|
||||
if cuda:
|
||||
model.cuda()
|
||||
|
||||
if use_globals:
|
||||
FID_MODEL = model
|
||||
|
||||
else:
|
||||
model = FID_MODEL
|
||||
|
||||
m1, s1 = _compute_statistics_of_images(images[0], model, batch_size,
|
||||
dims, cuda, keep_size=False)
|
||||
m2, s2 = _compute_statistics_of_images(images[1], model, batch_size,
|
||||
dims, cuda, keep_size=False)
|
||||
fid_value = calculate_frechet_distance(m1, s1, m2, s2)
|
||||
return fid_value
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = args.gpu
|
||||
|
||||
fid_value = calculate_fid_given_paths(args.path,
|
||||
args.batch_size,
|
||||
args.gpu != '',
|
||||
args.dims)
|
||||
print('FID: ', fid_value)
|
||||
@@ -0,0 +1,323 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torchvision import models
|
||||
|
||||
try:
|
||||
from torchvision.models.utils import load_state_dict_from_url
|
||||
except ImportError:
|
||||
from torch.utils.model_zoo import load_url as load_state_dict_from_url
|
||||
|
||||
# Inception weights ported to Pytorch from
|
||||
# http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz
|
||||
FID_WEIGHTS_URL = 'https://github.com/mseitzer/pytorch-fid/releases/download/fid_weights/pt_inception-2015-12-05-6726825d.pth'
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InceptionV3(nn.Module):
|
||||
"""Pretrained InceptionV3 network returning feature maps"""
|
||||
|
||||
# Index of default block of inception to return,
|
||||
# corresponds to output of final average pooling
|
||||
DEFAULT_BLOCK_INDEX = 3
|
||||
|
||||
# Maps feature dimensionality to their output blocks indices
|
||||
BLOCK_INDEX_BY_DIM = {
|
||||
64: 0, # First max pooling features
|
||||
192: 1, # Second max pooling featurs
|
||||
768: 2, # Pre-aux classifier features
|
||||
2048: 3 # Final average pooling features
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
output_blocks=[DEFAULT_BLOCK_INDEX],
|
||||
resize_input=True,
|
||||
normalize_input=True,
|
||||
requires_grad=False,
|
||||
use_fid_inception=True):
|
||||
"""Build pretrained InceptionV3
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output_blocks : list of int
|
||||
Indices of blocks to return features of. Possible values are:
|
||||
- 0: corresponds to output of first max pooling
|
||||
- 1: corresponds to output of second max pooling
|
||||
- 2: corresponds to output which is fed to aux classifier
|
||||
- 3: corresponds to output of final average pooling
|
||||
resize_input : bool
|
||||
If true, bilinearly resizes input to width and height 299 before
|
||||
feeding input to model. As the network without fully connected
|
||||
layers is fully convolutional, it should be able to handle inputs
|
||||
of arbitrary size, so resizing might not be strictly needed
|
||||
normalize_input : bool
|
||||
If true, scales the input from range (0, 1) to the range the
|
||||
pretrained Inception network expects, namely (-1, 1)
|
||||
requires_grad : bool
|
||||
If true, parameters of the model require gradients. Possibly useful
|
||||
for finetuning the network
|
||||
use_fid_inception : bool
|
||||
If true, uses the pretrained Inception model used in Tensorflow's
|
||||
FID implementation. If false, uses the pretrained Inception model
|
||||
available in torchvision. The FID Inception model has different
|
||||
weights and a slightly different structure from torchvision's
|
||||
Inception model. If you want to compute FID scores, you are
|
||||
strongly advised to set this parameter to true to get comparable
|
||||
results.
|
||||
"""
|
||||
super(InceptionV3, self).__init__()
|
||||
|
||||
self.resize_input = resize_input
|
||||
self.normalize_input = normalize_input
|
||||
self.output_blocks = sorted(output_blocks)
|
||||
self.last_needed_block = max(output_blocks)
|
||||
|
||||
assert self.last_needed_block <= 3, \
|
||||
'Last possible output block index is 3'
|
||||
|
||||
self.blocks = nn.ModuleList()
|
||||
|
||||
if use_fid_inception:
|
||||
inception = fid_inception_v3()
|
||||
else:
|
||||
inception = models.inception_v3(pretrained=True)
|
||||
|
||||
# Block 0: input to maxpool1
|
||||
block0 = [
|
||||
inception.Conv2d_1a_3x3,
|
||||
inception.Conv2d_2a_3x3,
|
||||
inception.Conv2d_2b_3x3,
|
||||
nn.MaxPool2d(kernel_size=3, stride=2)
|
||||
]
|
||||
self.blocks.append(nn.Sequential(*block0))
|
||||
|
||||
# Block 1: maxpool1 to maxpool2
|
||||
if self.last_needed_block >= 1:
|
||||
block1 = [
|
||||
inception.Conv2d_3b_1x1,
|
||||
inception.Conv2d_4a_3x3,
|
||||
nn.MaxPool2d(kernel_size=3, stride=2)
|
||||
]
|
||||
self.blocks.append(nn.Sequential(*block1))
|
||||
|
||||
# Block 2: maxpool2 to aux classifier
|
||||
if self.last_needed_block >= 2:
|
||||
block2 = [
|
||||
inception.Mixed_5b,
|
||||
inception.Mixed_5c,
|
||||
inception.Mixed_5d,
|
||||
inception.Mixed_6a,
|
||||
inception.Mixed_6b,
|
||||
inception.Mixed_6c,
|
||||
inception.Mixed_6d,
|
||||
inception.Mixed_6e,
|
||||
]
|
||||
self.blocks.append(nn.Sequential(*block2))
|
||||
|
||||
# Block 3: aux classifier to final avgpool
|
||||
if self.last_needed_block >= 3:
|
||||
block3 = [
|
||||
inception.Mixed_7a,
|
||||
inception.Mixed_7b,
|
||||
inception.Mixed_7c,
|
||||
nn.AdaptiveAvgPool2d(output_size=(1, 1))
|
||||
]
|
||||
self.blocks.append(nn.Sequential(*block3))
|
||||
|
||||
for param in self.parameters():
|
||||
param.requires_grad = requires_grad
|
||||
|
||||
def forward(self, inp):
|
||||
"""Get Inception feature maps
|
||||
|
||||
Parameters
|
||||
----------
|
||||
inp : torch.autograd.Variable
|
||||
Input tensor of shape Bx3xHxW. Values are expected to be in
|
||||
range (0, 1)
|
||||
|
||||
Returns
|
||||
-------
|
||||
List of torch.autograd.Variable, corresponding to the selected output
|
||||
block, sorted ascending by index
|
||||
"""
|
||||
outp = []
|
||||
x = inp
|
||||
|
||||
if self.resize_input:
|
||||
x = F.interpolate(x,
|
||||
size=(299, 299),
|
||||
mode='bilinear',
|
||||
align_corners=False)
|
||||
|
||||
if self.normalize_input:
|
||||
x = 2 * x - 1 # Scale from range (0, 1) to range (-1, 1)
|
||||
|
||||
for idx, block in enumerate(self.blocks):
|
||||
x = block(x)
|
||||
if idx in self.output_blocks:
|
||||
outp.append(x)
|
||||
|
||||
if idx == self.last_needed_block:
|
||||
break
|
||||
|
||||
return outp
|
||||
|
||||
|
||||
def fid_inception_v3():
|
||||
"""Build pretrained Inception model for FID computation
|
||||
|
||||
The Inception model for FID computation uses a different set of weights
|
||||
and has a slightly different structure than torchvision's Inception.
|
||||
|
||||
This method first constructs torchvision's Inception and then patches the
|
||||
necessary parts that are different in the FID Inception model.
|
||||
"""
|
||||
LOGGER.info('fid_inception_v3 called')
|
||||
inception = models.inception_v3(num_classes=1008,
|
||||
aux_logits=False,
|
||||
pretrained=False)
|
||||
LOGGER.info('models.inception_v3 done')
|
||||
inception.Mixed_5b = FIDInceptionA(192, pool_features=32)
|
||||
inception.Mixed_5c = FIDInceptionA(256, pool_features=64)
|
||||
inception.Mixed_5d = FIDInceptionA(288, pool_features=64)
|
||||
inception.Mixed_6b = FIDInceptionC(768, channels_7x7=128)
|
||||
inception.Mixed_6c = FIDInceptionC(768, channels_7x7=160)
|
||||
inception.Mixed_6d = FIDInceptionC(768, channels_7x7=160)
|
||||
inception.Mixed_6e = FIDInceptionC(768, channels_7x7=192)
|
||||
inception.Mixed_7b = FIDInceptionE_1(1280)
|
||||
inception.Mixed_7c = FIDInceptionE_2(2048)
|
||||
|
||||
LOGGER.info('fid_inception_v3 patching done')
|
||||
|
||||
state_dict = load_state_dict_from_url(FID_WEIGHTS_URL, progress=True)
|
||||
LOGGER.info('fid_inception_v3 weights downloaded')
|
||||
|
||||
inception.load_state_dict(state_dict)
|
||||
LOGGER.info('fid_inception_v3 weights loaded into model')
|
||||
|
||||
return inception
|
||||
|
||||
|
||||
class FIDInceptionA(models.inception.InceptionA):
|
||||
"""InceptionA block patched for FID computation"""
|
||||
def __init__(self, in_channels, pool_features):
|
||||
super(FIDInceptionA, self).__init__(in_channels, pool_features)
|
||||
|
||||
def forward(self, x):
|
||||
branch1x1 = self.branch1x1(x)
|
||||
|
||||
branch5x5 = self.branch5x5_1(x)
|
||||
branch5x5 = self.branch5x5_2(branch5x5)
|
||||
|
||||
branch3x3dbl = self.branch3x3dbl_1(x)
|
||||
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
|
||||
branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl)
|
||||
|
||||
# Patch: Tensorflow's average pool does not use the padded zero's in
|
||||
# its average calculation
|
||||
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1,
|
||||
count_include_pad=False)
|
||||
branch_pool = self.branch_pool(branch_pool)
|
||||
|
||||
outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool]
|
||||
return torch.cat(outputs, 1)
|
||||
|
||||
|
||||
class FIDInceptionC(models.inception.InceptionC):
|
||||
"""InceptionC block patched for FID computation"""
|
||||
def __init__(self, in_channels, channels_7x7):
|
||||
super(FIDInceptionC, self).__init__(in_channels, channels_7x7)
|
||||
|
||||
def forward(self, x):
|
||||
branch1x1 = self.branch1x1(x)
|
||||
|
||||
branch7x7 = self.branch7x7_1(x)
|
||||
branch7x7 = self.branch7x7_2(branch7x7)
|
||||
branch7x7 = self.branch7x7_3(branch7x7)
|
||||
|
||||
branch7x7dbl = self.branch7x7dbl_1(x)
|
||||
branch7x7dbl = self.branch7x7dbl_2(branch7x7dbl)
|
||||
branch7x7dbl = self.branch7x7dbl_3(branch7x7dbl)
|
||||
branch7x7dbl = self.branch7x7dbl_4(branch7x7dbl)
|
||||
branch7x7dbl = self.branch7x7dbl_5(branch7x7dbl)
|
||||
|
||||
# Patch: Tensorflow's average pool does not use the padded zero's in
|
||||
# its average calculation
|
||||
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1,
|
||||
count_include_pad=False)
|
||||
branch_pool = self.branch_pool(branch_pool)
|
||||
|
||||
outputs = [branch1x1, branch7x7, branch7x7dbl, branch_pool]
|
||||
return torch.cat(outputs, 1)
|
||||
|
||||
|
||||
class FIDInceptionE_1(models.inception.InceptionE):
|
||||
"""First InceptionE block patched for FID computation"""
|
||||
def __init__(self, in_channels):
|
||||
super(FIDInceptionE_1, self).__init__(in_channels)
|
||||
|
||||
def forward(self, x):
|
||||
branch1x1 = self.branch1x1(x)
|
||||
|
||||
branch3x3 = self.branch3x3_1(x)
|
||||
branch3x3 = [
|
||||
self.branch3x3_2a(branch3x3),
|
||||
self.branch3x3_2b(branch3x3),
|
||||
]
|
||||
branch3x3 = torch.cat(branch3x3, 1)
|
||||
|
||||
branch3x3dbl = self.branch3x3dbl_1(x)
|
||||
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
|
||||
branch3x3dbl = [
|
||||
self.branch3x3dbl_3a(branch3x3dbl),
|
||||
self.branch3x3dbl_3b(branch3x3dbl),
|
||||
]
|
||||
branch3x3dbl = torch.cat(branch3x3dbl, 1)
|
||||
|
||||
# Patch: Tensorflow's average pool does not use the padded zero's in
|
||||
# its average calculation
|
||||
branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1,
|
||||
count_include_pad=False)
|
||||
branch_pool = self.branch_pool(branch_pool)
|
||||
|
||||
outputs = [branch1x1, branch3x3, branch3x3dbl, branch_pool]
|
||||
return torch.cat(outputs, 1)
|
||||
|
||||
|
||||
class FIDInceptionE_2(models.inception.InceptionE):
|
||||
"""Second InceptionE block patched for FID computation"""
|
||||
def __init__(self, in_channels):
|
||||
super(FIDInceptionE_2, self).__init__(in_channels)
|
||||
|
||||
def forward(self, x):
|
||||
branch1x1 = self.branch1x1(x)
|
||||
|
||||
branch3x3 = self.branch3x3_1(x)
|
||||
branch3x3 = [
|
||||
self.branch3x3_2a(branch3x3),
|
||||
self.branch3x3_2b(branch3x3),
|
||||
]
|
||||
branch3x3 = torch.cat(branch3x3, 1)
|
||||
|
||||
branch3x3dbl = self.branch3x3dbl_1(x)
|
||||
branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
|
||||
branch3x3dbl = [
|
||||
self.branch3x3dbl_3a(branch3x3dbl),
|
||||
self.branch3x3dbl_3b(branch3x3dbl),
|
||||
]
|
||||
branch3x3dbl = torch.cat(branch3x3dbl, 1)
|
||||
|
||||
# Patch: The FID Inception model uses max pooling instead of average
|
||||
# pooling. This is likely an error in this specific Inception
|
||||
# implementation, as other Inception models use average pooling here
|
||||
# (which matches the description in the paper).
|
||||
branch_pool = F.max_pool2d(x, kernel_size=3, stride=1, padding=1)
|
||||
branch_pool = self.branch_pool(branch_pool)
|
||||
|
||||
outputs = [branch1x1, branch3x3, branch3x3dbl, branch_pool]
|
||||
return torch.cat(outputs, 1)
|
||||
@@ -0,0 +1,891 @@
|
||||
############################################################
|
||||
# The contents below have been combined using files in the #
|
||||
# following repository: #
|
||||
# https://github.com/richzhang/PerceptualSimilarity #
|
||||
############################################################
|
||||
|
||||
############################################################
|
||||
# __init__.py #
|
||||
############################################################
|
||||
|
||||
import numpy as np
|
||||
from skimage.metrics import structural_similarity
|
||||
import torch
|
||||
|
||||
from saicinpainting.utils import get_shape
|
||||
|
||||
|
||||
class PerceptualLoss(torch.nn.Module):
|
||||
def __init__(self, model='net-lin', net='alex', colorspace='rgb', model_path=None, spatial=False, use_gpu=True):
|
||||
# VGG using our perceptually-learned weights (LPIPS metric)
|
||||
# def __init__(self, model='net', net='vgg', use_gpu=True): # "default" way of using VGG as a perceptual loss
|
||||
super(PerceptualLoss, self).__init__()
|
||||
self.use_gpu = use_gpu
|
||||
self.spatial = spatial
|
||||
self.model = DistModel()
|
||||
self.model.initialize(model=model, net=net, use_gpu=use_gpu, colorspace=colorspace,
|
||||
model_path=model_path, spatial=self.spatial)
|
||||
|
||||
def forward(self, pred, target, normalize=True):
|
||||
"""
|
||||
Pred and target are Variables.
|
||||
If normalize is True, assumes the images are between [0,1] and then scales them between [-1,+1]
|
||||
If normalize is False, assumes the images are already between [-1,+1]
|
||||
Inputs pred and target are Nx3xHxW
|
||||
Output pytorch Variable N long
|
||||
"""
|
||||
|
||||
if normalize:
|
||||
target = 2 * target - 1
|
||||
pred = 2 * pred - 1
|
||||
|
||||
return self.model(target, pred)
|
||||
|
||||
|
||||
def normalize_tensor(in_feat, eps=1e-10):
|
||||
norm_factor = torch.sqrt(torch.sum(in_feat ** 2, dim=1, keepdim=True))
|
||||
return in_feat / (norm_factor + eps)
|
||||
|
||||
|
||||
def l2(p0, p1, range=255.):
|
||||
return .5 * np.mean((p0 / range - p1 / range) ** 2)
|
||||
|
||||
|
||||
def psnr(p0, p1, peak=255.):
|
||||
return 10 * np.log10(peak ** 2 / np.mean((1. * p0 - 1. * p1) ** 2))
|
||||
|
||||
|
||||
def dssim(p0, p1, range=255.):
|
||||
return (1 - compare_ssim(p0, p1, data_range=range, multichannel=True)) / 2.
|
||||
|
||||
|
||||
def rgb2lab(in_img, mean_cent=False):
|
||||
from skimage import color
|
||||
img_lab = color.rgb2lab(in_img)
|
||||
if (mean_cent):
|
||||
img_lab[:, :, 0] = img_lab[:, :, 0] - 50
|
||||
return img_lab
|
||||
|
||||
|
||||
def tensor2np(tensor_obj):
|
||||
# change dimension of a tensor object into a numpy array
|
||||
return tensor_obj[0].cpu().float().numpy().transpose((1, 2, 0))
|
||||
|
||||
|
||||
def np2tensor(np_obj):
|
||||
# change dimenion of np array into tensor array
|
||||
return torch.Tensor(np_obj[:, :, :, np.newaxis].transpose((3, 2, 0, 1)))
|
||||
|
||||
|
||||
def tensor2tensorlab(image_tensor, to_norm=True, mc_only=False):
|
||||
# image tensor to lab tensor
|
||||
from skimage import color
|
||||
|
||||
img = tensor2im(image_tensor)
|
||||
img_lab = color.rgb2lab(img)
|
||||
if (mc_only):
|
||||
img_lab[:, :, 0] = img_lab[:, :, 0] - 50
|
||||
if (to_norm and not mc_only):
|
||||
img_lab[:, :, 0] = img_lab[:, :, 0] - 50
|
||||
img_lab = img_lab / 100.
|
||||
|
||||
return np2tensor(img_lab)
|
||||
|
||||
|
||||
def tensorlab2tensor(lab_tensor, return_inbnd=False):
|
||||
from skimage import color
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
lab = tensor2np(lab_tensor) * 100.
|
||||
lab[:, :, 0] = lab[:, :, 0] + 50
|
||||
|
||||
rgb_back = 255. * np.clip(color.lab2rgb(lab.astype('float')), 0, 1)
|
||||
if (return_inbnd):
|
||||
# convert back to lab, see if we match
|
||||
lab_back = color.rgb2lab(rgb_back.astype('uint8'))
|
||||
mask = 1. * np.isclose(lab_back, lab, atol=2.)
|
||||
mask = np2tensor(np.prod(mask, axis=2)[:, :, np.newaxis])
|
||||
return (im2tensor(rgb_back), mask)
|
||||
else:
|
||||
return im2tensor(rgb_back)
|
||||
|
||||
|
||||
def rgb2lab(input):
|
||||
from skimage import color
|
||||
return color.rgb2lab(input / 255.)
|
||||
|
||||
|
||||
def tensor2im(image_tensor, imtype=np.uint8, cent=1., factor=255. / 2.):
|
||||
image_numpy = image_tensor[0].cpu().float().numpy()
|
||||
image_numpy = (np.transpose(image_numpy, (1, 2, 0)) + cent) * factor
|
||||
return image_numpy.astype(imtype)
|
||||
|
||||
|
||||
def im2tensor(image, imtype=np.uint8, cent=1., factor=255. / 2.):
|
||||
return torch.Tensor((image / factor - cent)
|
||||
[:, :, :, np.newaxis].transpose((3, 2, 0, 1)))
|
||||
|
||||
|
||||
def tensor2vec(vector_tensor):
|
||||
return vector_tensor.data.cpu().numpy()[:, :, 0, 0]
|
||||
|
||||
|
||||
def voc_ap(rec, prec, use_07_metric=False):
|
||||
""" ap = voc_ap(rec, prec, [use_07_metric])
|
||||
Compute VOC AP given precision and recall.
|
||||
If use_07_metric is true, uses the
|
||||
VOC 07 11 point method (default:False).
|
||||
"""
|
||||
if use_07_metric:
|
||||
# 11 point metric
|
||||
ap = 0.
|
||||
for t in np.arange(0., 1.1, 0.1):
|
||||
if np.sum(rec >= t) == 0:
|
||||
p = 0
|
||||
else:
|
||||
p = np.max(prec[rec >= t])
|
||||
ap = ap + p / 11.
|
||||
else:
|
||||
# correct AP calculation
|
||||
# first append sentinel values at the end
|
||||
mrec = np.concatenate(([0.], rec, [1.]))
|
||||
mpre = np.concatenate(([0.], prec, [0.]))
|
||||
|
||||
# compute the precision envelope
|
||||
for i in range(mpre.size - 1, 0, -1):
|
||||
mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i])
|
||||
|
||||
# to calculate area under PR curve, look for points
|
||||
# where X axis (recall) changes value
|
||||
i = np.where(mrec[1:] != mrec[:-1])[0]
|
||||
|
||||
# and sum (\Delta recall) * prec
|
||||
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])
|
||||
return ap
|
||||
|
||||
|
||||
def tensor2im(image_tensor, imtype=np.uint8, cent=1., factor=255. / 2.):
|
||||
# def tensor2im(image_tensor, imtype=np.uint8, cent=1., factor=1.):
|
||||
image_numpy = image_tensor[0].cpu().float().numpy()
|
||||
image_numpy = (np.transpose(image_numpy, (1, 2, 0)) + cent) * factor
|
||||
return image_numpy.astype(imtype)
|
||||
|
||||
|
||||
def im2tensor(image, imtype=np.uint8, cent=1., factor=255. / 2.):
|
||||
# def im2tensor(image, imtype=np.uint8, cent=1., factor=1.):
|
||||
return torch.Tensor((image / factor - cent)
|
||||
[:, :, :, np.newaxis].transpose((3, 2, 0, 1)))
|
||||
|
||||
|
||||
############################################################
|
||||
# base_model.py #
|
||||
############################################################
|
||||
|
||||
|
||||
class BaseModel(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def name(self):
|
||||
return 'BaseModel'
|
||||
|
||||
def initialize(self, use_gpu=True):
|
||||
self.use_gpu = use_gpu
|
||||
|
||||
def forward(self):
|
||||
pass
|
||||
|
||||
def get_image_paths(self):
|
||||
pass
|
||||
|
||||
def optimize_parameters(self):
|
||||
pass
|
||||
|
||||
def get_current_visuals(self):
|
||||
return self.input
|
||||
|
||||
def get_current_errors(self):
|
||||
return {}
|
||||
|
||||
def save(self, label):
|
||||
pass
|
||||
|
||||
# helper saving function that can be used by subclasses
|
||||
def save_network(self, network, path, network_label, epoch_label):
|
||||
save_filename = '%s_net_%s.pth' % (epoch_label, network_label)
|
||||
save_path = os.path.join(path, save_filename)
|
||||
torch.save(network.state_dict(), save_path)
|
||||
|
||||
# helper loading function that can be used by subclasses
|
||||
def load_network(self, network, network_label, epoch_label):
|
||||
save_filename = '%s_net_%s.pth' % (epoch_label, network_label)
|
||||
save_path = os.path.join(self.save_dir, save_filename)
|
||||
print('Loading network from %s' % save_path)
|
||||
network.load_state_dict(torch.load(save_path, map_location='cpu'))
|
||||
|
||||
def update_learning_rate():
|
||||
pass
|
||||
|
||||
def get_image_paths(self):
|
||||
return self.image_paths
|
||||
|
||||
def save_done(self, flag=False):
|
||||
np.save(os.path.join(self.save_dir, 'done_flag'), flag)
|
||||
np.savetxt(os.path.join(self.save_dir, 'done_flag'), [flag, ], fmt='%i')
|
||||
|
||||
|
||||
############################################################
|
||||
# dist_model.py #
|
||||
############################################################
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from scipy.ndimage import zoom
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class DistModel(BaseModel):
|
||||
def name(self):
|
||||
return self.model_name
|
||||
|
||||
def initialize(self, model='net-lin', net='alex', colorspace='Lab', pnet_rand=False, pnet_tune=False,
|
||||
model_path=None,
|
||||
use_gpu=True, printNet=False, spatial=False,
|
||||
is_train=False, lr=.0001, beta1=0.5, version='0.1'):
|
||||
'''
|
||||
INPUTS
|
||||
model - ['net-lin'] for linearly calibrated network
|
||||
['net'] for off-the-shelf network
|
||||
['L2'] for L2 distance in Lab colorspace
|
||||
['SSIM'] for ssim in RGB colorspace
|
||||
net - ['squeeze','alex','vgg']
|
||||
model_path - if None, will look in weights/[NET_NAME].pth
|
||||
colorspace - ['Lab','RGB'] colorspace to use for L2 and SSIM
|
||||
use_gpu - bool - whether or not to use a GPU
|
||||
printNet - bool - whether or not to print network architecture out
|
||||
spatial - bool - whether to output an array containing varying distances across spatial dimensions
|
||||
spatial_shape - if given, output spatial shape. if None then spatial shape is determined automatically via spatial_factor (see below).
|
||||
spatial_factor - if given, specifies upsampling factor relative to the largest spatial extent of a convolutional layer. if None then resized to size of input images.
|
||||
spatial_order - spline order of filter for upsampling in spatial mode, by default 1 (bilinear).
|
||||
is_train - bool - [True] for training mode
|
||||
lr - float - initial learning rate
|
||||
beta1 - float - initial momentum term for adam
|
||||
version - 0.1 for latest, 0.0 was original (with a bug)
|
||||
'''
|
||||
BaseModel.initialize(self, use_gpu=use_gpu)
|
||||
|
||||
self.model = model
|
||||
self.net = net
|
||||
self.is_train = is_train
|
||||
self.spatial = spatial
|
||||
self.model_name = '%s [%s]' % (model, net)
|
||||
|
||||
if (self.model == 'net-lin'): # pretrained net + linear layer
|
||||
self.net = PNetLin(pnet_rand=pnet_rand, pnet_tune=pnet_tune, pnet_type=net,
|
||||
use_dropout=True, spatial=spatial, version=version, lpips=True)
|
||||
kw = dict(map_location='cpu')
|
||||
if (model_path is None):
|
||||
import inspect
|
||||
model_path = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'models', 'lpips_models', f'{net}.pth'))
|
||||
|
||||
if (not is_train):
|
||||
self.net.load_state_dict(torch.load(model_path, **kw), strict=False)
|
||||
|
||||
elif (self.model == 'net'): # pretrained network
|
||||
self.net = PNetLin(pnet_rand=pnet_rand, pnet_type=net, lpips=False)
|
||||
elif (self.model in ['L2', 'l2']):
|
||||
self.net = L2(use_gpu=use_gpu, colorspace=colorspace) # not really a network, only for testing
|
||||
self.model_name = 'L2'
|
||||
elif (self.model in ['DSSIM', 'dssim', 'SSIM', 'ssim']):
|
||||
self.net = DSSIM(use_gpu=use_gpu, colorspace=colorspace)
|
||||
self.model_name = 'SSIM'
|
||||
else:
|
||||
raise ValueError("Model [%s] not recognized." % self.model)
|
||||
|
||||
self.trainable_parameters = list(self.net.parameters())
|
||||
|
||||
if self.is_train: # training mode
|
||||
# extra network on top to go from distances (d0,d1) => predicted human judgment (h*)
|
||||
self.rankLoss = BCERankingLoss()
|
||||
self.trainable_parameters += list(self.rankLoss.net.parameters())
|
||||
self.lr = lr
|
||||
self.old_lr = lr
|
||||
self.optimizer_net = torch.optim.Adam(self.trainable_parameters, lr=lr, betas=(beta1, 0.999))
|
||||
else: # test mode
|
||||
self.net.eval()
|
||||
|
||||
# if (use_gpu):
|
||||
# self.net.to(gpu_ids[0])
|
||||
# self.net = torch.nn.DataParallel(self.net, device_ids=gpu_ids)
|
||||
# if (self.is_train):
|
||||
# self.rankLoss = self.rankLoss.to(device=gpu_ids[0]) # just put this on GPU0
|
||||
|
||||
if (printNet):
|
||||
print('---------- Networks initialized -------------')
|
||||
print_network(self.net)
|
||||
print('-----------------------------------------------')
|
||||
|
||||
def forward(self, in0, in1, retPerLayer=False):
|
||||
''' Function computes the distance between image patches in0 and in1
|
||||
INPUTS
|
||||
in0, in1 - torch.Tensor object of shape Nx3xXxY - image patch scaled to [-1,1]
|
||||
OUTPUT
|
||||
computed distances between in0 and in1
|
||||
'''
|
||||
|
||||
return self.net(in0, in1, retPerLayer=retPerLayer)
|
||||
|
||||
# ***** TRAINING FUNCTIONS *****
|
||||
def optimize_parameters(self):
|
||||
self.forward_train()
|
||||
self.optimizer_net.zero_grad()
|
||||
self.backward_train()
|
||||
self.optimizer_net.step()
|
||||
self.clamp_weights()
|
||||
|
||||
def clamp_weights(self):
|
||||
for module in self.net.modules():
|
||||
if (hasattr(module, 'weight') and module.kernel_size == (1, 1)):
|
||||
module.weight.data = torch.clamp(module.weight.data, min=0)
|
||||
|
||||
def set_input(self, data):
|
||||
self.input_ref = data['ref']
|
||||
self.input_p0 = data['p0']
|
||||
self.input_p1 = data['p1']
|
||||
self.input_judge = data['judge']
|
||||
|
||||
# if (self.use_gpu):
|
||||
# self.input_ref = self.input_ref.to(device=self.gpu_ids[0])
|
||||
# self.input_p0 = self.input_p0.to(device=self.gpu_ids[0])
|
||||
# self.input_p1 = self.input_p1.to(device=self.gpu_ids[0])
|
||||
# self.input_judge = self.input_judge.to(device=self.gpu_ids[0])
|
||||
|
||||
# self.var_ref = Variable(self.input_ref, requires_grad=True)
|
||||
# self.var_p0 = Variable(self.input_p0, requires_grad=True)
|
||||
# self.var_p1 = Variable(self.input_p1, requires_grad=True)
|
||||
|
||||
def forward_train(self): # run forward pass
|
||||
# print(self.net.module.scaling_layer.shift)
|
||||
# print(torch.norm(self.net.module.net.slice1[0].weight).item(), torch.norm(self.net.module.lin0.model[1].weight).item())
|
||||
|
||||
assert False, "We shoud've not get here when using LPIPS as a metric"
|
||||
|
||||
self.d0 = self(self.var_ref, self.var_p0)
|
||||
self.d1 = self(self.var_ref, self.var_p1)
|
||||
self.acc_r = self.compute_accuracy(self.d0, self.d1, self.input_judge)
|
||||
|
||||
self.var_judge = Variable(1. * self.input_judge).view(self.d0.size())
|
||||
|
||||
self.loss_total = self.rankLoss(self.d0, self.d1, self.var_judge * 2. - 1.)
|
||||
|
||||
return self.loss_total
|
||||
|
||||
def backward_train(self):
|
||||
torch.mean(self.loss_total).backward()
|
||||
|
||||
def compute_accuracy(self, d0, d1, judge):
|
||||
''' d0, d1 are Variables, judge is a Tensor '''
|
||||
d1_lt_d0 = (d1 < d0).cpu().data.numpy().flatten()
|
||||
judge_per = judge.cpu().numpy().flatten()
|
||||
return d1_lt_d0 * judge_per + (1 - d1_lt_d0) * (1 - judge_per)
|
||||
|
||||
def get_current_errors(self):
|
||||
retDict = OrderedDict([('loss_total', self.loss_total.data.cpu().numpy()),
|
||||
('acc_r', self.acc_r)])
|
||||
|
||||
for key in retDict.keys():
|
||||
retDict[key] = np.mean(retDict[key])
|
||||
|
||||
return retDict
|
||||
|
||||
def get_current_visuals(self):
|
||||
zoom_factor = 256 / self.var_ref.data.size()[2]
|
||||
|
||||
ref_img = tensor2im(self.var_ref.data)
|
||||
p0_img = tensor2im(self.var_p0.data)
|
||||
p1_img = tensor2im(self.var_p1.data)
|
||||
|
||||
ref_img_vis = zoom(ref_img, [zoom_factor, zoom_factor, 1], order=0)
|
||||
p0_img_vis = zoom(p0_img, [zoom_factor, zoom_factor, 1], order=0)
|
||||
p1_img_vis = zoom(p1_img, [zoom_factor, zoom_factor, 1], order=0)
|
||||
|
||||
return OrderedDict([('ref', ref_img_vis),
|
||||
('p0', p0_img_vis),
|
||||
('p1', p1_img_vis)])
|
||||
|
||||
def save(self, path, label):
|
||||
if (self.use_gpu):
|
||||
self.save_network(self.net.module, path, '', label)
|
||||
else:
|
||||
self.save_network(self.net, path, '', label)
|
||||
self.save_network(self.rankLoss.net, path, 'rank', label)
|
||||
|
||||
def update_learning_rate(self, nepoch_decay):
|
||||
lrd = self.lr / nepoch_decay
|
||||
lr = self.old_lr - lrd
|
||||
|
||||
for param_group in self.optimizer_net.param_groups:
|
||||
param_group['lr'] = lr
|
||||
|
||||
print('update lr [%s] decay: %f -> %f' % (type, self.old_lr, lr))
|
||||
self.old_lr = lr
|
||||
|
||||
|
||||
def score_2afc_dataset(data_loader, func, name=''):
|
||||
''' Function computes Two Alternative Forced Choice (2AFC) score using
|
||||
distance function 'func' in dataset 'data_loader'
|
||||
INPUTS
|
||||
data_loader - CustomDatasetDataLoader object - contains a TwoAFCDataset inside
|
||||
func - callable distance function - calling d=func(in0,in1) should take 2
|
||||
pytorch tensors with shape Nx3xXxY, and return numpy array of length N
|
||||
OUTPUTS
|
||||
[0] - 2AFC score in [0,1], fraction of time func agrees with human evaluators
|
||||
[1] - dictionary with following elements
|
||||
d0s,d1s - N arrays containing distances between reference patch to perturbed patches
|
||||
gts - N array in [0,1], preferred patch selected by human evaluators
|
||||
(closer to "0" for left patch p0, "1" for right patch p1,
|
||||
"0.6" means 60pct people preferred right patch, 40pct preferred left)
|
||||
scores - N array in [0,1], corresponding to what percentage function agreed with humans
|
||||
CONSTS
|
||||
N - number of test triplets in data_loader
|
||||
'''
|
||||
|
||||
d0s = []
|
||||
d1s = []
|
||||
gts = []
|
||||
|
||||
for data in tqdm(data_loader.load_data(), desc=name):
|
||||
d0s += func(data['ref'], data['p0']).data.cpu().numpy().flatten().tolist()
|
||||
d1s += func(data['ref'], data['p1']).data.cpu().numpy().flatten().tolist()
|
||||
gts += data['judge'].cpu().numpy().flatten().tolist()
|
||||
|
||||
d0s = np.array(d0s)
|
||||
d1s = np.array(d1s)
|
||||
gts = np.array(gts)
|
||||
scores = (d0s < d1s) * (1. - gts) + (d1s < d0s) * gts + (d1s == d0s) * .5
|
||||
|
||||
return (np.mean(scores), dict(d0s=d0s, d1s=d1s, gts=gts, scores=scores))
|
||||
|
||||
|
||||
def score_jnd_dataset(data_loader, func, name=''):
|
||||
''' Function computes JND score using distance function 'func' in dataset 'data_loader'
|
||||
INPUTS
|
||||
data_loader - CustomDatasetDataLoader object - contains a JNDDataset inside
|
||||
func - callable distance function - calling d=func(in0,in1) should take 2
|
||||
pytorch tensors with shape Nx3xXxY, and return pytorch array of length N
|
||||
OUTPUTS
|
||||
[0] - JND score in [0,1], mAP score (area under precision-recall curve)
|
||||
[1] - dictionary with following elements
|
||||
ds - N array containing distances between two patches shown to human evaluator
|
||||
sames - N array containing fraction of people who thought the two patches were identical
|
||||
CONSTS
|
||||
N - number of test triplets in data_loader
|
||||
'''
|
||||
|
||||
ds = []
|
||||
gts = []
|
||||
|
||||
for data in tqdm(data_loader.load_data(), desc=name):
|
||||
ds += func(data['p0'], data['p1']).data.cpu().numpy().tolist()
|
||||
gts += data['same'].cpu().numpy().flatten().tolist()
|
||||
|
||||
sames = np.array(gts)
|
||||
ds = np.array(ds)
|
||||
|
||||
sorted_inds = np.argsort(ds)
|
||||
ds_sorted = ds[sorted_inds]
|
||||
sames_sorted = sames[sorted_inds]
|
||||
|
||||
TPs = np.cumsum(sames_sorted)
|
||||
FPs = np.cumsum(1 - sames_sorted)
|
||||
FNs = np.sum(sames_sorted) - TPs
|
||||
|
||||
precs = TPs / (TPs + FPs)
|
||||
recs = TPs / (TPs + FNs)
|
||||
score = voc_ap(recs, precs)
|
||||
|
||||
return (score, dict(ds=ds, sames=sames))
|
||||
|
||||
|
||||
############################################################
|
||||
# networks_basic.py #
|
||||
############################################################
|
||||
|
||||
import torch.nn as nn
|
||||
from torch.autograd import Variable
|
||||
import numpy as np
|
||||
|
||||
|
||||
def spatial_average(in_tens, keepdim=True):
|
||||
return in_tens.mean([2, 3], keepdim=keepdim)
|
||||
|
||||
|
||||
def upsample(in_tens, out_H=64): # assumes scale factor is same for H and W
|
||||
in_H = in_tens.shape[2]
|
||||
scale_factor = 1. * out_H / in_H
|
||||
|
||||
return nn.Upsample(scale_factor=scale_factor, mode='bilinear', align_corners=False)(in_tens)
|
||||
|
||||
|
||||
# Learned perceptual metric
|
||||
class PNetLin(nn.Module):
|
||||
def __init__(self, pnet_type='vgg', pnet_rand=False, pnet_tune=False, use_dropout=True, spatial=False,
|
||||
version='0.1', lpips=True):
|
||||
super(PNetLin, self).__init__()
|
||||
|
||||
self.pnet_type = pnet_type
|
||||
self.pnet_tune = pnet_tune
|
||||
self.pnet_rand = pnet_rand
|
||||
self.spatial = spatial
|
||||
self.lpips = lpips
|
||||
self.version = version
|
||||
self.scaling_layer = ScalingLayer()
|
||||
|
||||
if (self.pnet_type in ['vgg', 'vgg16']):
|
||||
net_type = vgg16
|
||||
self.chns = [64, 128, 256, 512, 512]
|
||||
elif (self.pnet_type == 'alex'):
|
||||
net_type = alexnet
|
||||
self.chns = [64, 192, 384, 256, 256]
|
||||
elif (self.pnet_type == 'squeeze'):
|
||||
net_type = squeezenet
|
||||
self.chns = [64, 128, 256, 384, 384, 512, 512]
|
||||
self.L = len(self.chns)
|
||||
|
||||
self.net = net_type(pretrained=not self.pnet_rand, requires_grad=self.pnet_tune)
|
||||
|
||||
if (lpips):
|
||||
self.lin0 = NetLinLayer(self.chns[0], use_dropout=use_dropout)
|
||||
self.lin1 = NetLinLayer(self.chns[1], use_dropout=use_dropout)
|
||||
self.lin2 = NetLinLayer(self.chns[2], use_dropout=use_dropout)
|
||||
self.lin3 = NetLinLayer(self.chns[3], use_dropout=use_dropout)
|
||||
self.lin4 = NetLinLayer(self.chns[4], use_dropout=use_dropout)
|
||||
self.lins = [self.lin0, self.lin1, self.lin2, self.lin3, self.lin4]
|
||||
if (self.pnet_type == 'squeeze'): # 7 layers for squeezenet
|
||||
self.lin5 = NetLinLayer(self.chns[5], use_dropout=use_dropout)
|
||||
self.lin6 = NetLinLayer(self.chns[6], use_dropout=use_dropout)
|
||||
self.lins += [self.lin5, self.lin6]
|
||||
|
||||
def forward(self, in0, in1, retPerLayer=False):
|
||||
# v0.0 - original release had a bug, where input was not scaled
|
||||
in0_input, in1_input = (self.scaling_layer(in0), self.scaling_layer(in1)) if self.version == '0.1' else (
|
||||
in0, in1)
|
||||
outs0, outs1 = self.net(in0_input), self.net(in1_input)
|
||||
feats0, feats1, diffs = {}, {}, {}
|
||||
|
||||
for kk in range(self.L):
|
||||
feats0[kk], feats1[kk] = normalize_tensor(outs0[kk]), normalize_tensor(outs1[kk])
|
||||
diffs[kk] = (feats0[kk] - feats1[kk]) ** 2
|
||||
|
||||
if (self.lpips):
|
||||
if (self.spatial):
|
||||
res = [upsample(self.lins[kk].model(diffs[kk]), out_H=in0.shape[2]) for kk in range(self.L)]
|
||||
else:
|
||||
res = [spatial_average(self.lins[kk].model(diffs[kk]), keepdim=True) for kk in range(self.L)]
|
||||
else:
|
||||
if (self.spatial):
|
||||
res = [upsample(diffs[kk].sum(dim=1, keepdim=True), out_H=in0.shape[2]) for kk in range(self.L)]
|
||||
else:
|
||||
res = [spatial_average(diffs[kk].sum(dim=1, keepdim=True), keepdim=True) for kk in range(self.L)]
|
||||
|
||||
val = res[0]
|
||||
for l in range(1, self.L):
|
||||
val += res[l]
|
||||
|
||||
if (retPerLayer):
|
||||
return (val, res)
|
||||
else:
|
||||
return val
|
||||
|
||||
|
||||
class ScalingLayer(nn.Module):
|
||||
def __init__(self):
|
||||
super(ScalingLayer, self).__init__()
|
||||
self.register_buffer('shift', torch.Tensor([-.030, -.088, -.188])[None, :, None, None])
|
||||
self.register_buffer('scale', torch.Tensor([.458, .448, .450])[None, :, None, None])
|
||||
|
||||
def forward(self, inp):
|
||||
return (inp - self.shift) / self.scale
|
||||
|
||||
|
||||
class NetLinLayer(nn.Module):
|
||||
''' A single linear layer which does a 1x1 conv '''
|
||||
|
||||
def __init__(self, chn_in, chn_out=1, use_dropout=False):
|
||||
super(NetLinLayer, self).__init__()
|
||||
|
||||
layers = [nn.Dropout(), ] if (use_dropout) else []
|
||||
layers += [nn.Conv2d(chn_in, chn_out, 1, stride=1, padding=0, bias=False), ]
|
||||
self.model = nn.Sequential(*layers)
|
||||
|
||||
|
||||
class Dist2LogitLayer(nn.Module):
|
||||
''' takes 2 distances, puts through fc layers, spits out value between [0,1] (if use_sigmoid is True) '''
|
||||
|
||||
def __init__(self, chn_mid=32, use_sigmoid=True):
|
||||
super(Dist2LogitLayer, self).__init__()
|
||||
|
||||
layers = [nn.Conv2d(5, chn_mid, 1, stride=1, padding=0, bias=True), ]
|
||||
layers += [nn.LeakyReLU(0.2, True), ]
|
||||
layers += [nn.Conv2d(chn_mid, chn_mid, 1, stride=1, padding=0, bias=True), ]
|
||||
layers += [nn.LeakyReLU(0.2, True), ]
|
||||
layers += [nn.Conv2d(chn_mid, 1, 1, stride=1, padding=0, bias=True), ]
|
||||
if (use_sigmoid):
|
||||
layers += [nn.Sigmoid(), ]
|
||||
self.model = nn.Sequential(*layers)
|
||||
|
||||
def forward(self, d0, d1, eps=0.1):
|
||||
return self.model(torch.cat((d0, d1, d0 - d1, d0 / (d1 + eps), d1 / (d0 + eps)), dim=1))
|
||||
|
||||
|
||||
class BCERankingLoss(nn.Module):
|
||||
def __init__(self, chn_mid=32):
|
||||
super(BCERankingLoss, self).__init__()
|
||||
self.net = Dist2LogitLayer(chn_mid=chn_mid)
|
||||
# self.parameters = list(self.net.parameters())
|
||||
self.loss = torch.nn.BCELoss()
|
||||
|
||||
def forward(self, d0, d1, judge):
|
||||
per = (judge + 1.) / 2.
|
||||
self.logit = self.net(d0, d1)
|
||||
return self.loss(self.logit, per)
|
||||
|
||||
|
||||
# L2, DSSIM metrics
|
||||
class FakeNet(nn.Module):
|
||||
def __init__(self, use_gpu=True, colorspace='Lab'):
|
||||
super(FakeNet, self).__init__()
|
||||
self.use_gpu = use_gpu
|
||||
self.colorspace = colorspace
|
||||
|
||||
|
||||
class L2(FakeNet):
|
||||
|
||||
def forward(self, in0, in1, retPerLayer=None):
|
||||
assert (in0.size()[0] == 1) # currently only supports batchSize 1
|
||||
|
||||
if (self.colorspace == 'RGB'):
|
||||
(N, C, X, Y) = in0.size()
|
||||
value = torch.mean(torch.mean(torch.mean((in0 - in1) ** 2, dim=1).view(N, 1, X, Y), dim=2).view(N, 1, 1, Y),
|
||||
dim=3).view(N)
|
||||
return value
|
||||
elif (self.colorspace == 'Lab'):
|
||||
value = l2(tensor2np(tensor2tensorlab(in0.data, to_norm=False)),
|
||||
tensor2np(tensor2tensorlab(in1.data, to_norm=False)), range=100.).astype('float')
|
||||
ret_var = Variable(torch.Tensor((value,)))
|
||||
# if (self.use_gpu):
|
||||
# ret_var = ret_var.cuda()
|
||||
return ret_var
|
||||
|
||||
|
||||
class DSSIM(FakeNet):
|
||||
|
||||
def forward(self, in0, in1, retPerLayer=None):
|
||||
assert (in0.size()[0] == 1) # currently only supports batchSize 1
|
||||
|
||||
if (self.colorspace == 'RGB'):
|
||||
value = dssim(1. * tensor2im(in0.data), 1. * tensor2im(in1.data), range=255.).astype('float')
|
||||
elif (self.colorspace == 'Lab'):
|
||||
value = dssim(tensor2np(tensor2tensorlab(in0.data, to_norm=False)),
|
||||
tensor2np(tensor2tensorlab(in1.data, to_norm=False)), range=100.).astype('float')
|
||||
ret_var = Variable(torch.Tensor((value,)))
|
||||
# if (self.use_gpu):
|
||||
# ret_var = ret_var.cuda()
|
||||
return ret_var
|
||||
|
||||
|
||||
def print_network(net):
|
||||
num_params = 0
|
||||
for param in net.parameters():
|
||||
num_params += param.numel()
|
||||
print('Network', net)
|
||||
print('Total number of parameters: %d' % num_params)
|
||||
|
||||
|
||||
############################################################
|
||||
# pretrained_networks.py #
|
||||
############################################################
|
||||
|
||||
from collections import namedtuple
|
||||
import torch
|
||||
from torchvision import models as tv
|
||||
|
||||
|
||||
class squeezenet(torch.nn.Module):
|
||||
def __init__(self, requires_grad=False, pretrained=True):
|
||||
super(squeezenet, self).__init__()
|
||||
pretrained_features = tv.squeezenet1_1(pretrained=pretrained).features
|
||||
self.slice1 = torch.nn.Sequential()
|
||||
self.slice2 = torch.nn.Sequential()
|
||||
self.slice3 = torch.nn.Sequential()
|
||||
self.slice4 = torch.nn.Sequential()
|
||||
self.slice5 = torch.nn.Sequential()
|
||||
self.slice6 = torch.nn.Sequential()
|
||||
self.slice7 = torch.nn.Sequential()
|
||||
self.N_slices = 7
|
||||
for x in range(2):
|
||||
self.slice1.add_module(str(x), pretrained_features[x])
|
||||
for x in range(2, 5):
|
||||
self.slice2.add_module(str(x), pretrained_features[x])
|
||||
for x in range(5, 8):
|
||||
self.slice3.add_module(str(x), pretrained_features[x])
|
||||
for x in range(8, 10):
|
||||
self.slice4.add_module(str(x), pretrained_features[x])
|
||||
for x in range(10, 11):
|
||||
self.slice5.add_module(str(x), pretrained_features[x])
|
||||
for x in range(11, 12):
|
||||
self.slice6.add_module(str(x), pretrained_features[x])
|
||||
for x in range(12, 13):
|
||||
self.slice7.add_module(str(x), pretrained_features[x])
|
||||
if not requires_grad:
|
||||
for param in self.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
def forward(self, X):
|
||||
h = self.slice1(X)
|
||||
h_relu1 = h
|
||||
h = self.slice2(h)
|
||||
h_relu2 = h
|
||||
h = self.slice3(h)
|
||||
h_relu3 = h
|
||||
h = self.slice4(h)
|
||||
h_relu4 = h
|
||||
h = self.slice5(h)
|
||||
h_relu5 = h
|
||||
h = self.slice6(h)
|
||||
h_relu6 = h
|
||||
h = self.slice7(h)
|
||||
h_relu7 = h
|
||||
vgg_outputs = namedtuple("SqueezeOutputs", ['relu1', 'relu2', 'relu3', 'relu4', 'relu5', 'relu6', 'relu7'])
|
||||
out = vgg_outputs(h_relu1, h_relu2, h_relu3, h_relu4, h_relu5, h_relu6, h_relu7)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class alexnet(torch.nn.Module):
|
||||
def __init__(self, requires_grad=False, pretrained=True):
|
||||
super(alexnet, self).__init__()
|
||||
alexnet_pretrained_features = tv.alexnet(pretrained=pretrained).features
|
||||
self.slice1 = torch.nn.Sequential()
|
||||
self.slice2 = torch.nn.Sequential()
|
||||
self.slice3 = torch.nn.Sequential()
|
||||
self.slice4 = torch.nn.Sequential()
|
||||
self.slice5 = torch.nn.Sequential()
|
||||
self.N_slices = 5
|
||||
for x in range(2):
|
||||
self.slice1.add_module(str(x), alexnet_pretrained_features[x])
|
||||
for x in range(2, 5):
|
||||
self.slice2.add_module(str(x), alexnet_pretrained_features[x])
|
||||
for x in range(5, 8):
|
||||
self.slice3.add_module(str(x), alexnet_pretrained_features[x])
|
||||
for x in range(8, 10):
|
||||
self.slice4.add_module(str(x), alexnet_pretrained_features[x])
|
||||
for x in range(10, 12):
|
||||
self.slice5.add_module(str(x), alexnet_pretrained_features[x])
|
||||
if not requires_grad:
|
||||
for param in self.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
def forward(self, X):
|
||||
h = self.slice1(X)
|
||||
h_relu1 = h
|
||||
h = self.slice2(h)
|
||||
h_relu2 = h
|
||||
h = self.slice3(h)
|
||||
h_relu3 = h
|
||||
h = self.slice4(h)
|
||||
h_relu4 = h
|
||||
h = self.slice5(h)
|
||||
h_relu5 = h
|
||||
alexnet_outputs = namedtuple("AlexnetOutputs", ['relu1', 'relu2', 'relu3', 'relu4', 'relu5'])
|
||||
out = alexnet_outputs(h_relu1, h_relu2, h_relu3, h_relu4, h_relu5)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class vgg16(torch.nn.Module):
|
||||
def __init__(self, requires_grad=False, pretrained=True):
|
||||
super(vgg16, self).__init__()
|
||||
vgg_pretrained_features = tv.vgg16(pretrained=pretrained).features
|
||||
self.slice1 = torch.nn.Sequential()
|
||||
self.slice2 = torch.nn.Sequential()
|
||||
self.slice3 = torch.nn.Sequential()
|
||||
self.slice4 = torch.nn.Sequential()
|
||||
self.slice5 = torch.nn.Sequential()
|
||||
self.N_slices = 5
|
||||
for x in range(4):
|
||||
self.slice1.add_module(str(x), vgg_pretrained_features[x])
|
||||
for x in range(4, 9):
|
||||
self.slice2.add_module(str(x), vgg_pretrained_features[x])
|
||||
for x in range(9, 16):
|
||||
self.slice3.add_module(str(x), vgg_pretrained_features[x])
|
||||
for x in range(16, 23):
|
||||
self.slice4.add_module(str(x), vgg_pretrained_features[x])
|
||||
for x in range(23, 30):
|
||||
self.slice5.add_module(str(x), vgg_pretrained_features[x])
|
||||
if not requires_grad:
|
||||
for param in self.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
def forward(self, X):
|
||||
h = self.slice1(X)
|
||||
h_relu1_2 = h
|
||||
h = self.slice2(h)
|
||||
h_relu2_2 = h
|
||||
h = self.slice3(h)
|
||||
h_relu3_3 = h
|
||||
h = self.slice4(h)
|
||||
h_relu4_3 = h
|
||||
h = self.slice5(h)
|
||||
h_relu5_3 = h
|
||||
vgg_outputs = namedtuple("VggOutputs", ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3', 'relu5_3'])
|
||||
out = vgg_outputs(h_relu1_2, h_relu2_2, h_relu3_3, h_relu4_3, h_relu5_3)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class resnet(torch.nn.Module):
|
||||
def __init__(self, requires_grad=False, pretrained=True, num=18):
|
||||
super(resnet, self).__init__()
|
||||
if (num == 18):
|
||||
self.net = tv.resnet18(pretrained=pretrained)
|
||||
elif (num == 34):
|
||||
self.net = tv.resnet34(pretrained=pretrained)
|
||||
elif (num == 50):
|
||||
self.net = tv.resnet50(pretrained=pretrained)
|
||||
elif (num == 101):
|
||||
self.net = tv.resnet101(pretrained=pretrained)
|
||||
elif (num == 152):
|
||||
self.net = tv.resnet152(pretrained=pretrained)
|
||||
self.N_slices = 5
|
||||
|
||||
self.conv1 = self.net.conv1
|
||||
self.bn1 = self.net.bn1
|
||||
self.relu = self.net.relu
|
||||
self.maxpool = self.net.maxpool
|
||||
self.layer1 = self.net.layer1
|
||||
self.layer2 = self.net.layer2
|
||||
self.layer3 = self.net.layer3
|
||||
self.layer4 = self.net.layer4
|
||||
|
||||
def forward(self, X):
|
||||
h = self.conv1(X)
|
||||
h = self.bn1(h)
|
||||
h = self.relu(h)
|
||||
h_relu1 = h
|
||||
h = self.maxpool(h)
|
||||
h = self.layer1(h)
|
||||
h_conv2 = h
|
||||
h = self.layer2(h)
|
||||
h_conv3 = h
|
||||
h = self.layer3(h)
|
||||
h_conv4 = h
|
||||
h = self.layer4(h)
|
||||
h_conv5 = h
|
||||
|
||||
outputs = namedtuple("Outputs", ['relu1', 'conv2', 'conv3', 'conv4', 'conv5'])
|
||||
out = outputs(h_relu1, h_conv2, h_conv3, h_conv4, h_conv5)
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,74 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class SSIM(torch.nn.Module):
|
||||
"""SSIM. Modified from:
|
||||
https://github.com/Po-Hsun-Su/pytorch-ssim/blob/master/pytorch_ssim/__init__.py
|
||||
"""
|
||||
|
||||
def __init__(self, window_size=11, size_average=True):
|
||||
super().__init__()
|
||||
self.window_size = window_size
|
||||
self.size_average = size_average
|
||||
self.channel = 1
|
||||
self.register_buffer('window', self._create_window(window_size, self.channel))
|
||||
|
||||
def forward(self, img1, img2):
|
||||
assert len(img1.shape) == 4
|
||||
|
||||
channel = img1.size()[1]
|
||||
|
||||
if channel == self.channel and self.window.data.type() == img1.data.type():
|
||||
window = self.window
|
||||
else:
|
||||
window = self._create_window(self.window_size, channel)
|
||||
|
||||
# window = window.to(img1.get_device())
|
||||
window = window.type_as(img1)
|
||||
|
||||
self.window = window
|
||||
self.channel = channel
|
||||
|
||||
return self._ssim(img1, img2, window, self.window_size, channel, self.size_average)
|
||||
|
||||
def _gaussian(self, window_size, sigma):
|
||||
gauss = torch.Tensor([
|
||||
np.exp(-(x - (window_size // 2)) ** 2 / float(2 * sigma ** 2)) for x in range(window_size)
|
||||
])
|
||||
return gauss / gauss.sum()
|
||||
|
||||
def _create_window(self, window_size, channel):
|
||||
_1D_window = self._gaussian(window_size, 1.5).unsqueeze(1)
|
||||
_2D_window = _1D_window.mm(_1D_window.t()).float().unsqueeze(0).unsqueeze(0)
|
||||
return _2D_window.expand(channel, 1, window_size, window_size).contiguous()
|
||||
|
||||
def _ssim(self, img1, img2, window, window_size, channel, size_average=True):
|
||||
mu1 = F.conv2d(img1, window, padding=(window_size // 2), groups=channel)
|
||||
mu2 = F.conv2d(img2, window, padding=(window_size // 2), groups=channel)
|
||||
|
||||
mu1_sq = mu1.pow(2)
|
||||
mu2_sq = mu2.pow(2)
|
||||
mu1_mu2 = mu1 * mu2
|
||||
|
||||
sigma1_sq = F.conv2d(
|
||||
img1 * img1, window, padding=(window_size // 2), groups=channel) - mu1_sq
|
||||
sigma2_sq = F.conv2d(
|
||||
img2 * img2, window, padding=(window_size // 2), groups=channel) - mu2_sq
|
||||
sigma12 = F.conv2d(
|
||||
img1 * img2, window, padding=(window_size // 2), groups=channel) - mu1_mu2
|
||||
|
||||
C1 = 0.01 ** 2
|
||||
C2 = 0.03 ** 2
|
||||
|
||||
ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / \
|
||||
((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2))
|
||||
|
||||
if size_average:
|
||||
return ssim_map.mean()
|
||||
|
||||
return ssim_map.mean(1).mean(1).mean(1)
|
||||
|
||||
def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
|
||||
return
|
||||
@@ -0,0 +1,27 @@
|
||||
# Current algorithm
|
||||
|
||||
## Choice of mask objects
|
||||
|
||||
For identification of the objects which are suitable for mask obtaining, panoptic segmentation model
|
||||
from [detectron2](https://github.com/facebookresearch/detectron2) trained on COCO. Categories of the detected instances
|
||||
belong either to "stuff" or "things" types. We consider that instances of objects should have category belong
|
||||
to "things". Besides, we set upper bound on area which is taken by the object — we consider that too big
|
||||
area indicates either of the instance being a background or a main object which should not be removed.
|
||||
|
||||
## Choice of position for mask
|
||||
|
||||
We consider that input image has size 2^n x 2^m. We downsample it using
|
||||
[COUNTLESS](https://github.com/william-silversmith/countless) algorithm so the width is equal to
|
||||
64 = 2^8 = 2^{downsample_levels}.
|
||||
|
||||
### Augmentation
|
||||
|
||||
There are several parameters for augmentation:
|
||||
- Scaling factor. We limit scaling to the case when a mask after scaling with pivot point in its center fits inside the
|
||||
image completely.
|
||||
-
|
||||
|
||||
### Shift
|
||||
|
||||
|
||||
## Select
|
||||
@@ -0,0 +1 @@
|
||||
results
|
||||
@@ -0,0 +1,25 @@
|
||||
[](https://travis-ci.org/william-silversmith/countless)
|
||||
|
||||
Python COUNTLESS Downsampling
|
||||
=============================
|
||||
|
||||
To install:
|
||||
|
||||
`pip install -r requirements.txt`
|
||||
|
||||
To test:
|
||||
|
||||
`python test.py`
|
||||
|
||||
To benchmark countless2d:
|
||||
|
||||
`python python/countless2d.py python/images/gray_segmentation.png`
|
||||
|
||||
To benchmark countless3d:
|
||||
|
||||
`python python/countless3d.py`
|
||||
|
||||
Adjust N and the list of algorithms inside each script to modify the run parameters.
|
||||
|
||||
|
||||
Python3 is slightly faster than Python2.
|
||||
@@ -0,0 +1,529 @@
|
||||
from __future__ import print_function, division
|
||||
|
||||
"""
|
||||
COUNTLESS performance test in Python.
|
||||
|
||||
python countless2d.py ./images/NAMEOFIMAGE
|
||||
"""
|
||||
|
||||
import six
|
||||
from six.moves import range
|
||||
from collections import defaultdict
|
||||
from functools import reduce
|
||||
import operator
|
||||
import io
|
||||
import os
|
||||
from PIL import Image
|
||||
import math
|
||||
import numpy as np
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from tqdm import tqdm
|
||||
from scipy import ndimage
|
||||
|
||||
def simplest_countless(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab = a * (a == b) # PICK(A,B)
|
||||
ac = a * (a == c) # PICK(A,C)
|
||||
bc = b * (b == c) # PICK(B,C)
|
||||
|
||||
a = ab | ac | bc # Bitwise OR, safe b/c non-matches are zeroed
|
||||
|
||||
return a + (a == 0) * d # AB || AC || BC || D
|
||||
|
||||
def quick_countless(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab_ac = a * ((a == b) | (a == c)) # PICK(A,B) || PICK(A,C) w/ optimization
|
||||
bc = b * (b == c) # PICK(B,C)
|
||||
|
||||
a = ab_ac | bc # (PICK(A,B) || PICK(A,C)) or PICK(B,C)
|
||||
return a + (a == 0) * d # AB || AC || BC || D
|
||||
|
||||
def quickest_countless(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab_ac = a * ((a == b) | (a == c)) # PICK(A,B) || PICK(A,C) w/ optimization
|
||||
ab_ac |= b * (b == c) # PICK(B,C)
|
||||
return ab_ac + (ab_ac == 0) * d # AB || AC || BC || D
|
||||
|
||||
def quick_countless_xor(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab = a ^ (a ^ b) # a or b
|
||||
ab += (ab != a) * ((ab ^ (ab ^ c)) - b) # b or c
|
||||
ab += (ab == c) * ((ab ^ (ab ^ d)) - c) # c or d
|
||||
return ab
|
||||
|
||||
def stippled_countless(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm
|
||||
that treats zero as "background" and inflates lone
|
||||
pixels.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab_ac = a * ((a == b) | (a == c)) # PICK(A,B) || PICK(A,C) w/ optimization
|
||||
ab_ac |= b * (b == c) # PICK(B,C)
|
||||
|
||||
nonzero = a + (a == 0) * (b + (b == 0) * c)
|
||||
return ab_ac + (ab_ac == 0) * (d + (d == 0) * nonzero) # AB || AC || BC || D
|
||||
|
||||
def zero_corrected_countless(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
# allows us to prevent losing 1/2 a bit of information
|
||||
# at the top end by using a bigger type. Without this 255 is handled incorrectly.
|
||||
data, upgraded = upgrade_type(data)
|
||||
|
||||
# offset from zero, raw countless doesn't handle 0 correctly
|
||||
# we'll remove the extra 1 at the end.
|
||||
data += 1
|
||||
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab = a * (a == b) # PICK(A,B)
|
||||
ac = a * (a == c) # PICK(A,C)
|
||||
bc = b * (b == c) # PICK(B,C)
|
||||
|
||||
a = ab | ac | bc # Bitwise OR, safe b/c non-matches are zeroed
|
||||
|
||||
result = a + (a == 0) * d - 1 # a or d - 1
|
||||
|
||||
if upgraded:
|
||||
return downgrade_type(result)
|
||||
|
||||
# only need to reset data if we weren't upgraded
|
||||
# b/c no copy was made in that case
|
||||
data -= 1
|
||||
|
||||
return result
|
||||
|
||||
def countless_extreme(data):
|
||||
nonzeros = np.count_nonzero(data)
|
||||
# print("nonzeros", nonzeros)
|
||||
|
||||
N = reduce(operator.mul, data.shape)
|
||||
|
||||
if nonzeros == N:
|
||||
print("quick")
|
||||
return quick_countless(data)
|
||||
elif np.count_nonzero(data + 1) == N:
|
||||
print("quick")
|
||||
# print("upper", nonzeros)
|
||||
return quick_countless(data)
|
||||
else:
|
||||
return countless(data)
|
||||
|
||||
|
||||
def countless(data):
|
||||
"""
|
||||
Vectorized implementation of downsampling a 2D
|
||||
image by 2 on each side using the COUNTLESS algorithm.
|
||||
|
||||
data is a 2D numpy array with even dimensions.
|
||||
"""
|
||||
# allows us to prevent losing 1/2 a bit of information
|
||||
# at the top end by using a bigger type. Without this 255 is handled incorrectly.
|
||||
data, upgraded = upgrade_type(data)
|
||||
|
||||
# offset from zero, raw countless doesn't handle 0 correctly
|
||||
# we'll remove the extra 1 at the end.
|
||||
data += 1
|
||||
|
||||
sections = []
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
a, b, c, d = sections
|
||||
|
||||
ab_ac = a * ((a == b) | (a == c)) # PICK(A,B) || PICK(A,C) w/ optimization
|
||||
ab_ac |= b * (b == c) # PICK(B,C)
|
||||
result = ab_ac + (ab_ac == 0) * d - 1 # (matches or d) - 1
|
||||
|
||||
if upgraded:
|
||||
return downgrade_type(result)
|
||||
|
||||
# only need to reset data if we weren't upgraded
|
||||
# b/c no copy was made in that case
|
||||
data -= 1
|
||||
|
||||
return result
|
||||
|
||||
def upgrade_type(arr):
|
||||
dtype = arr.dtype
|
||||
|
||||
if dtype == np.uint8:
|
||||
return arr.astype(np.uint16), True
|
||||
elif dtype == np.uint16:
|
||||
return arr.astype(np.uint32), True
|
||||
elif dtype == np.uint32:
|
||||
return arr.astype(np.uint64), True
|
||||
|
||||
return arr, False
|
||||
|
||||
def downgrade_type(arr):
|
||||
dtype = arr.dtype
|
||||
|
||||
if dtype == np.uint64:
|
||||
return arr.astype(np.uint32)
|
||||
elif dtype == np.uint32:
|
||||
return arr.astype(np.uint16)
|
||||
elif dtype == np.uint16:
|
||||
return arr.astype(np.uint8)
|
||||
|
||||
return arr
|
||||
|
||||
def odd_to_even(image):
|
||||
"""
|
||||
To facilitate 2x2 downsampling segmentation, change an odd sized image into an even sized one.
|
||||
Works by mirroring the starting 1 pixel edge of the image on odd shaped sides.
|
||||
|
||||
e.g. turn a 3x3x5 image into a 4x4x5 (the x and y are what are getting downsampled)
|
||||
|
||||
For example: [ 3, 2, 4 ] => [ 3, 3, 2, 4 ] which is now easy to downsample.
|
||||
|
||||
"""
|
||||
shape = np.array(image.shape)
|
||||
|
||||
offset = (shape % 2)[:2] # x,y offset
|
||||
|
||||
# detect if we're dealing with an even
|
||||
# image. if so it's fine, just return.
|
||||
if not np.any(offset):
|
||||
return image
|
||||
|
||||
oddshape = image.shape[:2] + offset
|
||||
oddshape = np.append(oddshape, shape[2:])
|
||||
oddshape = oddshape.astype(int)
|
||||
|
||||
newimg = np.empty(shape=oddshape, dtype=image.dtype)
|
||||
|
||||
ox,oy = offset
|
||||
sx,sy = oddshape
|
||||
|
||||
newimg[0,0] = image[0,0] # corner
|
||||
newimg[ox:sx,0] = image[:,0] # x axis line
|
||||
newimg[0,oy:sy] = image[0,:] # y axis line
|
||||
|
||||
return newimg
|
||||
|
||||
def counting(array):
|
||||
factor = (2, 2, 1)
|
||||
shape = array.shape
|
||||
|
||||
while len(shape) < 4:
|
||||
array = np.expand_dims(array, axis=-1)
|
||||
shape = array.shape
|
||||
|
||||
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(shape, factor))
|
||||
output = np.zeros(output_shape, dtype=array.dtype)
|
||||
|
||||
for chan in range(0, shape[3]):
|
||||
for z in range(0, shape[2]):
|
||||
for x in range(0, shape[0], 2):
|
||||
for y in range(0, shape[1], 2):
|
||||
block = array[ x:x+2, y:y+2, z, chan ] # 2x2 block
|
||||
|
||||
hashtable = defaultdict(int)
|
||||
for subx, suby in np.ndindex(block.shape[0], block.shape[1]):
|
||||
hashtable[block[subx, suby]] += 1
|
||||
|
||||
best = (0, 0)
|
||||
for segid, val in six.iteritems(hashtable):
|
||||
if best[1] < val:
|
||||
best = (segid, val)
|
||||
|
||||
output[ x // 2, y // 2, chan ] = best[0]
|
||||
|
||||
return output
|
||||
|
||||
def ndzoom(array):
|
||||
if len(array.shape) == 3:
|
||||
ratio = ( 1 / 2.0, 1 / 2.0, 1.0 )
|
||||
else:
|
||||
ratio = ( 1 / 2.0, 1 / 2.0)
|
||||
return ndimage.interpolation.zoom(array, ratio, order=1)
|
||||
|
||||
def countless_if(array):
|
||||
factor = (2, 2, 1)
|
||||
shape = array.shape
|
||||
|
||||
if len(shape) < 3:
|
||||
array = array[ :,:, np.newaxis ]
|
||||
shape = array.shape
|
||||
|
||||
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(shape, factor))
|
||||
output = np.zeros(output_shape, dtype=array.dtype)
|
||||
|
||||
for chan in range(0, shape[2]):
|
||||
for x in range(0, shape[0], 2):
|
||||
for y in range(0, shape[1], 2):
|
||||
block = array[ x:x+2, y:y+2, chan ] # 2x2 block
|
||||
|
||||
if block[0,0] == block[1,0]:
|
||||
pick = block[0,0]
|
||||
elif block[0,0] == block[0,1]:
|
||||
pick = block[0,0]
|
||||
elif block[1,0] == block[0,1]:
|
||||
pick = block[1,0]
|
||||
else:
|
||||
pick = block[1,1]
|
||||
|
||||
output[ x // 2, y // 2, chan ] = pick
|
||||
|
||||
return np.squeeze(output)
|
||||
|
||||
def downsample_with_averaging(array):
|
||||
"""
|
||||
Downsample x by factor using averaging.
|
||||
|
||||
@return: The downsampled array, of the same type as x.
|
||||
"""
|
||||
|
||||
if len(array.shape) == 3:
|
||||
factor = (2,2,1)
|
||||
else:
|
||||
factor = (2,2)
|
||||
|
||||
if np.array_equal(factor[:3], np.array([1,1,1])):
|
||||
return array
|
||||
|
||||
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(array.shape, factor))
|
||||
temp = np.zeros(output_shape, float)
|
||||
counts = np.zeros(output_shape, np.int)
|
||||
for offset in np.ndindex(factor):
|
||||
part = array[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
indexing_expr = tuple(np.s_[:s] for s in part.shape)
|
||||
temp[indexing_expr] += part
|
||||
counts[indexing_expr] += 1
|
||||
return np.cast[array.dtype](temp / counts)
|
||||
|
||||
def downsample_with_max_pooling(array):
|
||||
|
||||
factor = (2,2)
|
||||
|
||||
if np.all(np.array(factor, int) == 1):
|
||||
return array
|
||||
|
||||
sections = []
|
||||
|
||||
for offset in np.ndindex(factor):
|
||||
part = array[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
output = sections[0].copy()
|
||||
|
||||
for section in sections[1:]:
|
||||
np.maximum(output, section, output)
|
||||
|
||||
return output
|
||||
|
||||
def striding(array):
|
||||
"""Downsample x by factor using striding.
|
||||
|
||||
@return: The downsampled array, of the same type as x.
|
||||
"""
|
||||
factor = (2,2)
|
||||
if np.all(np.array(factor, int) == 1):
|
||||
return array
|
||||
return array[tuple(np.s_[::f] for f in factor)]
|
||||
|
||||
def benchmark():
|
||||
filename = sys.argv[1]
|
||||
img = Image.open(filename)
|
||||
data = np.array(img.getdata(), dtype=np.uint8)
|
||||
|
||||
if len(data.shape) == 1:
|
||||
n_channels = 1
|
||||
reshape = (img.height, img.width)
|
||||
else:
|
||||
n_channels = min(data.shape[1], 3)
|
||||
data = data[:, :n_channels]
|
||||
reshape = (img.height, img.width, n_channels)
|
||||
|
||||
data = data.reshape(reshape).astype(np.uint8)
|
||||
|
||||
methods = [
|
||||
simplest_countless,
|
||||
quick_countless,
|
||||
quick_countless_xor,
|
||||
quickest_countless,
|
||||
stippled_countless,
|
||||
zero_corrected_countless,
|
||||
countless,
|
||||
downsample_with_averaging,
|
||||
downsample_with_max_pooling,
|
||||
ndzoom,
|
||||
striding,
|
||||
# countless_if,
|
||||
# counting,
|
||||
]
|
||||
|
||||
formats = {
|
||||
1: 'L',
|
||||
3: 'RGB',
|
||||
4: 'RGBA'
|
||||
}
|
||||
|
||||
if not os.path.exists('./results'):
|
||||
os.mkdir('./results')
|
||||
|
||||
N = 500
|
||||
img_size = float(img.width * img.height) / 1024.0 / 1024.0
|
||||
print("N = %d, %dx%d (%.2f MPx) %d chan, %s" % (N, img.width, img.height, img_size, n_channels, filename))
|
||||
print("Algorithm\tMPx/sec\tMB/sec\tSec")
|
||||
for fn in methods:
|
||||
print(fn.__name__, end='')
|
||||
sys.stdout.flush()
|
||||
|
||||
start = time.time()
|
||||
# tqdm is here to show you what's going on the first time you run it.
|
||||
# Feel free to remove it to get slightly more accurate timing results.
|
||||
for _ in tqdm(range(N), desc=fn.__name__, disable=True):
|
||||
result = fn(data)
|
||||
end = time.time()
|
||||
print("\r", end='')
|
||||
|
||||
total_time = (end - start)
|
||||
mpx = N * img_size / total_time
|
||||
mbytes = N * img_size * n_channels / total_time
|
||||
# Output in tab separated format to enable copy-paste into excel/numbers
|
||||
print("%s\t%.3f\t%.3f\t%.2f" % (fn.__name__, mpx, mbytes, total_time))
|
||||
outimg = Image.fromarray(np.squeeze(result), formats[n_channels])
|
||||
outimg.save('./results/{}.png'.format(fn.__name__, "PNG"))
|
||||
|
||||
if __name__ == '__main__':
|
||||
benchmark()
|
||||
|
||||
|
||||
# Example results:
|
||||
# N = 5, 1024x1024 (1.00 MPx) 1 chan, images/gray_segmentation.png
|
||||
# Function MPx/sec MB/sec Sec
|
||||
# simplest_countless 752.855 752.855 0.01
|
||||
# quick_countless 920.328 920.328 0.01
|
||||
# zero_corrected_countless 534.143 534.143 0.01
|
||||
# countless 644.247 644.247 0.01
|
||||
# downsample_with_averaging 372.575 372.575 0.01
|
||||
# downsample_with_max_pooling 974.060 974.060 0.01
|
||||
# ndzoom 137.517 137.517 0.04
|
||||
# striding 38550.588 38550.588 0.00
|
||||
# countless_if 4.377 4.377 1.14
|
||||
# counting 0.117 0.117 42.85
|
||||
|
||||
# Run without non-numpy implementations:
|
||||
# N = 2000, 1024x1024 (1.00 MPx) 1 chan, images/gray_segmentation.png
|
||||
# Algorithm MPx/sec MB/sec Sec
|
||||
# simplest_countless 800.522 800.522 2.50
|
||||
# quick_countless 945.420 945.420 2.12
|
||||
# quickest_countless 947.256 947.256 2.11
|
||||
# stippled_countless 544.049 544.049 3.68
|
||||
# zero_corrected_countless 575.310 575.310 3.48
|
||||
# countless 646.684 646.684 3.09
|
||||
# downsample_with_averaging 385.132 385.132 5.19
|
||||
# downsample_with_max_poolin 988.361 988.361 2.02
|
||||
# ndzoom 163.104 163.104 12.26
|
||||
# striding 81589.340 81589.340 0.02
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
from six.moves import range
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import io
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from itertools import combinations
|
||||
from functools import reduce
|
||||
from tqdm import tqdm
|
||||
|
||||
from memory_profiler import profile
|
||||
|
||||
def countless5(a,b,c,d,e):
|
||||
"""First stage of generalizing from countless2d.
|
||||
|
||||
You have five slots: A, B, C, D, E
|
||||
|
||||
You can decide if something is the winner by first checking for
|
||||
matches of three, then matches of two, then picking just one if
|
||||
the other two tries fail. In countless2d, you just check for matches
|
||||
of two and then pick one of them otherwise.
|
||||
|
||||
Unfortunately, you need to check ABC, ABD, ABE, BCD, BDE, & CDE.
|
||||
Then you need to check AB, AC, AD, BC, BD
|
||||
We skip checking E because if none of these match, we pick E. We can
|
||||
skip checking AE, BE, CE, DE since if any of those match, E is our boy
|
||||
so it's redundant.
|
||||
|
||||
So countless grows cominatorially in complexity.
|
||||
"""
|
||||
sections = [ a,b,c,d,e ]
|
||||
|
||||
p2 = lambda q,r: q * (q == r) # q if p == q else 0
|
||||
p3 = lambda q,r,s: q * ( (q == r) & (r == s) ) # q if q == r == s else 0
|
||||
|
||||
lor = lambda x,y: x + (x == 0) * y
|
||||
|
||||
results3 = ( p3(x,y,z) for x,y,z in combinations(sections, 3) )
|
||||
results3 = reduce(lor, results3)
|
||||
|
||||
results2 = ( p2(x,y) for x,y in combinations(sections[:-1], 2) )
|
||||
results2 = reduce(lor, results2)
|
||||
|
||||
return reduce(lor, (results3, results2, e))
|
||||
|
||||
def countless8(a,b,c,d,e,f,g,h):
|
||||
"""Extend countless5 to countless8. Same deal, except we also
|
||||
need to check for matches of length 4."""
|
||||
sections = [ a, b, c, d, e, f, g, h ]
|
||||
|
||||
p2 = lambda q,r: q * (q == r)
|
||||
p3 = lambda q,r,s: q * ( (q == r) & (r == s) )
|
||||
p4 = lambda p,q,r,s: p * ( (p == q) & (q == r) & (r == s) )
|
||||
|
||||
lor = lambda x,y: x + (x == 0) * y
|
||||
|
||||
results4 = ( p4(x,y,z,w) for x,y,z,w in combinations(sections, 4) )
|
||||
results4 = reduce(lor, results4)
|
||||
|
||||
results3 = ( p3(x,y,z) for x,y,z in combinations(sections, 3) )
|
||||
results3 = reduce(lor, results3)
|
||||
|
||||
# We can always use our shortcut of omitting the last element
|
||||
# for N choose 2
|
||||
results2 = ( p2(x,y) for x,y in combinations(sections[:-1], 2) )
|
||||
results2 = reduce(lor, results2)
|
||||
|
||||
return reduce(lor, [ results4, results3, results2, h ])
|
||||
|
||||
def dynamic_countless3d(data):
|
||||
"""countless8 + dynamic programming. ~2x faster"""
|
||||
sections = []
|
||||
|
||||
# shift zeros up one so they don't interfere with bitwise operators
|
||||
# we'll shift down at the end
|
||||
data += 1
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
pick = lambda a,b: a * (a == b)
|
||||
lor = lambda x,y: x + (x == 0) * y
|
||||
|
||||
subproblems2 = {}
|
||||
|
||||
results2 = None
|
||||
for x,y in combinations(range(7), 2):
|
||||
res = pick(sections[x], sections[y])
|
||||
subproblems2[(x,y)] = res
|
||||
if results2 is not None:
|
||||
results2 += (results2 == 0) * res
|
||||
else:
|
||||
results2 = res
|
||||
|
||||
subproblems3 = {}
|
||||
|
||||
results3 = None
|
||||
for x,y,z in combinations(range(8), 3):
|
||||
res = pick(subproblems2[(x,y)], sections[z])
|
||||
|
||||
if z != 7:
|
||||
subproblems3[(x,y,z)] = res
|
||||
|
||||
if results3 is not None:
|
||||
results3 += (results3 == 0) * res
|
||||
else:
|
||||
results3 = res
|
||||
|
||||
results3 = reduce(lor, (results3, results2, sections[-1]))
|
||||
|
||||
# free memory
|
||||
results2 = None
|
||||
subproblems2 = None
|
||||
res = None
|
||||
|
||||
results4 = ( pick(subproblems3[(x,y,z)], sections[w]) for x,y,z,w in combinations(range(8), 4) )
|
||||
results4 = reduce(lor, results4)
|
||||
subproblems3 = None # free memory
|
||||
|
||||
final_result = lor(results4, results3) - 1
|
||||
data -= 1
|
||||
return final_result
|
||||
|
||||
def countless3d(data):
|
||||
"""Now write countless8 in such a way that it could be used
|
||||
to process an image."""
|
||||
sections = []
|
||||
|
||||
# shift zeros up one so they don't interfere with bitwise operators
|
||||
# we'll shift down at the end
|
||||
data += 1
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
factor = (2,2,2)
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
p2 = lambda q,r: q * (q == r)
|
||||
p3 = lambda q,r,s: q * ( (q == r) & (r == s) )
|
||||
p4 = lambda p,q,r,s: p * ( (p == q) & (q == r) & (r == s) )
|
||||
|
||||
lor = lambda x,y: x + (x == 0) * y
|
||||
|
||||
results4 = ( p4(x,y,z,w) for x,y,z,w in combinations(sections, 4) )
|
||||
results4 = reduce(lor, results4)
|
||||
|
||||
results3 = ( p3(x,y,z) for x,y,z in combinations(sections, 3) )
|
||||
results3 = reduce(lor, results3)
|
||||
|
||||
results2 = ( p2(x,y) for x,y in combinations(sections[:-1], 2) )
|
||||
results2 = reduce(lor, results2)
|
||||
|
||||
final_result = reduce(lor, (results4, results3, results2, sections[-1])) - 1
|
||||
data -= 1
|
||||
return final_result
|
||||
|
||||
def countless_generalized(data, factor):
|
||||
assert len(data.shape) == len(factor)
|
||||
|
||||
sections = []
|
||||
|
||||
mode_of = reduce(lambda x,y: x * y, factor)
|
||||
majority = int(math.ceil(float(mode_of) / 2))
|
||||
|
||||
data += 1
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
def pick(elements):
|
||||
eq = ( elements[i] == elements[i+1] for i in range(len(elements) - 1) )
|
||||
anded = reduce(lambda p,q: p & q, eq)
|
||||
return elements[0] * anded
|
||||
|
||||
def logical_or(x,y):
|
||||
return x + (x == 0) * y
|
||||
|
||||
result = ( pick(combo) for combo in combinations(sections, majority) )
|
||||
result = reduce(logical_or, result)
|
||||
for i in range(majority - 1, 3-1, -1): # 3-1 b/c of exclusive bounds
|
||||
partial_result = ( pick(combo) for combo in combinations(sections, i) )
|
||||
partial_result = reduce(logical_or, partial_result)
|
||||
result = logical_or(result, partial_result)
|
||||
|
||||
partial_result = ( pick(combo) for combo in combinations(sections[:-1], 2) )
|
||||
partial_result = reduce(logical_or, partial_result)
|
||||
result = logical_or(result, partial_result)
|
||||
|
||||
result = logical_or(result, sections[-1]) - 1
|
||||
data -= 1
|
||||
return result
|
||||
|
||||
def dynamic_countless_generalized(data, factor):
|
||||
assert len(data.shape) == len(factor)
|
||||
|
||||
sections = []
|
||||
|
||||
mode_of = reduce(lambda x,y: x * y, factor)
|
||||
majority = int(math.ceil(float(mode_of) / 2))
|
||||
|
||||
data += 1 # offset from zero
|
||||
|
||||
# This loop splits the 2D array apart into four arrays that are
|
||||
# all the result of striding by 2 and offset by (0,0), (0,1), (1,0),
|
||||
# and (1,1) representing the A, B, C, and D positions from Figure 1.
|
||||
for offset in np.ndindex(factor):
|
||||
part = data[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
pick = lambda a,b: a * (a == b)
|
||||
lor = lambda x,y: x + (x == 0) * y # logical or
|
||||
|
||||
subproblems = [ {}, {} ]
|
||||
results2 = None
|
||||
for x,y in combinations(range(len(sections) - 1), 2):
|
||||
res = pick(sections[x], sections[y])
|
||||
subproblems[0][(x,y)] = res
|
||||
if results2 is not None:
|
||||
results2 = lor(results2, res)
|
||||
else:
|
||||
results2 = res
|
||||
|
||||
results = [ results2 ]
|
||||
for r in range(3, majority+1):
|
||||
r_results = None
|
||||
for combo in combinations(range(len(sections)), r):
|
||||
res = pick(subproblems[0][combo[:-1]], sections[combo[-1]])
|
||||
|
||||
if combo[-1] != len(sections) - 1:
|
||||
subproblems[1][combo] = res
|
||||
|
||||
if r_results is not None:
|
||||
r_results = lor(r_results, res)
|
||||
else:
|
||||
r_results = res
|
||||
results.append(r_results)
|
||||
subproblems[0] = subproblems[1]
|
||||
subproblems[1] = {}
|
||||
|
||||
results.reverse()
|
||||
final_result = lor(reduce(lor, results), sections[-1]) - 1
|
||||
data -= 1
|
||||
return final_result
|
||||
|
||||
def downsample_with_averaging(array):
|
||||
"""
|
||||
Downsample x by factor using averaging.
|
||||
|
||||
@return: The downsampled array, of the same type as x.
|
||||
"""
|
||||
factor = (2,2,2)
|
||||
|
||||
if np.array_equal(factor[:3], np.array([1,1,1])):
|
||||
return array
|
||||
|
||||
output_shape = tuple(int(math.ceil(s / f)) for s, f in zip(array.shape, factor))
|
||||
temp = np.zeros(output_shape, float)
|
||||
counts = np.zeros(output_shape, np.int)
|
||||
for offset in np.ndindex(factor):
|
||||
part = array[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
indexing_expr = tuple(np.s_[:s] for s in part.shape)
|
||||
temp[indexing_expr] += part
|
||||
counts[indexing_expr] += 1
|
||||
return np.cast[array.dtype](temp / counts)
|
||||
|
||||
def downsample_with_max_pooling(array):
|
||||
|
||||
factor = (2,2,2)
|
||||
|
||||
sections = []
|
||||
|
||||
for offset in np.ndindex(factor):
|
||||
part = array[tuple(np.s_[o::f] for o, f in zip(offset, factor))]
|
||||
sections.append(part)
|
||||
|
||||
output = sections[0].copy()
|
||||
|
||||
for section in sections[1:]:
|
||||
np.maximum(output, section, output)
|
||||
|
||||
return output
|
||||
|
||||
def striding(array):
|
||||
"""Downsample x by factor using striding.
|
||||
|
||||
@return: The downsampled array, of the same type as x.
|
||||
"""
|
||||
factor = (2,2,2)
|
||||
if np.all(np.array(factor, int) == 1):
|
||||
return array
|
||||
return array[tuple(np.s_[::f] for f in factor)]
|
||||
|
||||
def benchmark():
|
||||
def countless3d_generalized(img):
|
||||
return countless_generalized(img, (2,8,1))
|
||||
def countless3d_dynamic_generalized(img):
|
||||
return dynamic_countless_generalized(img, (8,8,1))
|
||||
|
||||
methods = [
|
||||
# countless3d,
|
||||
# dynamic_countless3d,
|
||||
countless3d_generalized,
|
||||
# countless3d_dynamic_generalized,
|
||||
# striding,
|
||||
# downsample_with_averaging,
|
||||
# downsample_with_max_pooling
|
||||
]
|
||||
|
||||
data = np.zeros(shape=(16**2, 16**2, 16**2), dtype=np.uint8) + 1
|
||||
|
||||
N = 5
|
||||
|
||||
print('Algorithm\tMPx\tMB/sec\tSec\tN=%d' % N)
|
||||
|
||||
for fn in methods:
|
||||
start = time.time()
|
||||
for _ in range(N):
|
||||
result = fn(data)
|
||||
end = time.time()
|
||||
|
||||
total_time = (end - start)
|
||||
mpx = N * float(data.shape[0] * data.shape[1] * data.shape[2]) / total_time / 1024.0 / 1024.0
|
||||
mbytes = mpx * np.dtype(data.dtype).itemsize
|
||||
# Output in tab separated format to enable copy-paste into excel/numbers
|
||||
print("%s\t%.3f\t%.3f\t%.2f" % (fn.__name__, mpx, mbytes, total_time))
|
||||
|
||||
if __name__ == '__main__':
|
||||
benchmark()
|
||||
|
||||
# Algorithm MPx MB/sec Sec N=5
|
||||
# countless3d 10.564 10.564 60.58
|
||||
# dynamic_countless3d 22.717 22.717 28.17
|
||||
# countless3d_generalized 9.702 9.702 65.96
|
||||
# countless3d_dynamic_generalized 22.720 22.720 28.17
|
||||
# striding 253360.506 253360.506 0.00
|
||||
# downsample_with_averaging 224.098 224.098 2.86
|
||||
# downsample_with_max_pooling 690.474 690.474 0.93
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,7 @@
|
||||
Pillow>=6.2.0
|
||||
numpy>=1.16
|
||||
scipy
|
||||
tqdm
|
||||
memory_profiler
|
||||
six
|
||||
pytest
|
||||
@@ -0,0 +1,195 @@
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
|
||||
import countless2d
|
||||
import countless3d
|
||||
|
||||
def test_countless2d():
|
||||
def test_all_cases(fn, test_zero):
|
||||
case1 = np.array([ [ 1, 2 ], [ 3, 4 ] ]).reshape((2,2,1,1)) # all different
|
||||
case2 = np.array([ [ 1, 1 ], [ 2, 3 ] ]).reshape((2,2,1,1)) # two are same
|
||||
case1z = np.array([ [ 0, 1 ], [ 2, 3 ] ]).reshape((2,2,1,1)) # all different
|
||||
case2z = np.array([ [ 0, 0 ], [ 2, 3 ] ]).reshape((2,2,1,1)) # two are same
|
||||
case3 = np.array([ [ 1, 1 ], [ 2, 2 ] ]).reshape((2,2,1,1)) # two groups are same
|
||||
case4 = np.array([ [ 1, 2 ], [ 2, 2 ] ]).reshape((2,2,1,1)) # 3 are the same
|
||||
case5 = np.array([ [ 5, 5 ], [ 5, 5 ] ]).reshape((2,2,1,1)) # all are the same
|
||||
|
||||
is_255_handled = np.array([ [ 255, 255 ], [ 1, 2 ] ], dtype=np.uint8).reshape((2,2,1,1))
|
||||
|
||||
test = lambda case: fn(case)
|
||||
|
||||
if test_zero:
|
||||
assert test(case1z) == [[[[3]]]] # d
|
||||
assert test(case2z) == [[[[0]]]] # a==b
|
||||
else:
|
||||
assert test(case1) == [[[[4]]]] # d
|
||||
assert test(case2) == [[[[1]]]] # a==b
|
||||
|
||||
assert test(case3) == [[[[1]]]] # a==b
|
||||
assert test(case4) == [[[[2]]]] # b==c
|
||||
assert test(case5) == [[[[5]]]] # a==b
|
||||
|
||||
assert test(is_255_handled) == [[[[255]]]]
|
||||
|
||||
assert fn(case1).dtype == case1.dtype
|
||||
|
||||
test_all_cases(countless2d.simplest_countless, False)
|
||||
test_all_cases(countless2d.quick_countless, False)
|
||||
test_all_cases(countless2d.quickest_countless, False)
|
||||
test_all_cases(countless2d.stippled_countless, False)
|
||||
|
||||
|
||||
|
||||
methods = [
|
||||
countless2d.zero_corrected_countless,
|
||||
countless2d.countless,
|
||||
countless2d.countless_if,
|
||||
# countless2d.counting, # counting doesn't respect order so harder to write a test
|
||||
]
|
||||
|
||||
for fn in methods:
|
||||
print(fn.__name__)
|
||||
test_all_cases(fn, True)
|
||||
|
||||
def test_stippled_countless2d():
|
||||
a = np.array([ [ 1, 2 ], [ 3, 4 ] ]).reshape((2,2,1,1))
|
||||
b = np.array([ [ 0, 2 ], [ 3, 4 ] ]).reshape((2,2,1,1))
|
||||
c = np.array([ [ 1, 0 ], [ 3, 4 ] ]).reshape((2,2,1,1))
|
||||
d = np.array([ [ 1, 2 ], [ 0, 4 ] ]).reshape((2,2,1,1))
|
||||
e = np.array([ [ 1, 2 ], [ 3, 0 ] ]).reshape((2,2,1,1))
|
||||
f = np.array([ [ 0, 0 ], [ 3, 4 ] ]).reshape((2,2,1,1))
|
||||
g = np.array([ [ 0, 2 ], [ 0, 4 ] ]).reshape((2,2,1,1))
|
||||
h = np.array([ [ 0, 2 ], [ 3, 0 ] ]).reshape((2,2,1,1))
|
||||
i = np.array([ [ 1, 0 ], [ 0, 4 ] ]).reshape((2,2,1,1))
|
||||
j = np.array([ [ 1, 2 ], [ 0, 0 ] ]).reshape((2,2,1,1))
|
||||
k = np.array([ [ 1, 0 ], [ 3, 0 ] ]).reshape((2,2,1,1))
|
||||
l = np.array([ [ 1, 0 ], [ 0, 0 ] ]).reshape((2,2,1,1))
|
||||
m = np.array([ [ 0, 2 ], [ 0, 0 ] ]).reshape((2,2,1,1))
|
||||
n = np.array([ [ 0, 0 ], [ 3, 0 ] ]).reshape((2,2,1,1))
|
||||
o = np.array([ [ 0, 0 ], [ 0, 4 ] ]).reshape((2,2,1,1))
|
||||
z = np.array([ [ 0, 0 ], [ 0, 0 ] ]).reshape((2,2,1,1))
|
||||
|
||||
test = countless2d.stippled_countless
|
||||
|
||||
# Note: We only tested non-matching cases above,
|
||||
# cases f,g,h,i,j,k prove their duals work as well
|
||||
# b/c if two pixels are black, either one can be chosen
|
||||
# if they are different or the same.
|
||||
|
||||
assert test(a) == [[[[4]]]]
|
||||
assert test(b) == [[[[4]]]]
|
||||
assert test(c) == [[[[4]]]]
|
||||
assert test(d) == [[[[4]]]]
|
||||
assert test(e) == [[[[1]]]]
|
||||
assert test(f) == [[[[4]]]]
|
||||
assert test(g) == [[[[4]]]]
|
||||
assert test(h) == [[[[2]]]]
|
||||
assert test(i) == [[[[4]]]]
|
||||
assert test(j) == [[[[1]]]]
|
||||
assert test(k) == [[[[1]]]]
|
||||
assert test(l) == [[[[1]]]]
|
||||
assert test(m) == [[[[2]]]]
|
||||
assert test(n) == [[[[3]]]]
|
||||
assert test(o) == [[[[4]]]]
|
||||
assert test(z) == [[[[0]]]]
|
||||
|
||||
bc = np.array([ [ 0, 2 ], [ 2, 4 ] ]).reshape((2,2,1,1))
|
||||
bd = np.array([ [ 0, 2 ], [ 3, 2 ] ]).reshape((2,2,1,1))
|
||||
cd = np.array([ [ 0, 2 ], [ 3, 3 ] ]).reshape((2,2,1,1))
|
||||
|
||||
assert test(bc) == [[[[2]]]]
|
||||
assert test(bd) == [[[[2]]]]
|
||||
assert test(cd) == [[[[3]]]]
|
||||
|
||||
ab = np.array([ [ 1, 1 ], [ 0, 4 ] ]).reshape((2,2,1,1))
|
||||
ac = np.array([ [ 1, 2 ], [ 1, 0 ] ]).reshape((2,2,1,1))
|
||||
ad = np.array([ [ 1, 0 ], [ 3, 1 ] ]).reshape((2,2,1,1))
|
||||
|
||||
assert test(ab) == [[[[1]]]]
|
||||
assert test(ac) == [[[[1]]]]
|
||||
assert test(ad) == [[[[1]]]]
|
||||
|
||||
def test_countless3d():
|
||||
def test_all_cases(fn):
|
||||
alldifferent = [
|
||||
[
|
||||
[1,2],
|
||||
[3,4],
|
||||
],
|
||||
[
|
||||
[5,6],
|
||||
[7,8]
|
||||
]
|
||||
]
|
||||
allsame = [
|
||||
[
|
||||
[1,1],
|
||||
[1,1],
|
||||
],
|
||||
[
|
||||
[1,1],
|
||||
[1,1]
|
||||
]
|
||||
]
|
||||
|
||||
assert fn(np.array(alldifferent)) == [[[8]]]
|
||||
assert fn(np.array(allsame)) == [[[1]]]
|
||||
|
||||
twosame = deepcopy(alldifferent)
|
||||
twosame[1][1][0] = 2
|
||||
|
||||
assert fn(np.array(twosame)) == [[[2]]]
|
||||
|
||||
threemixed = [
|
||||
[
|
||||
[3,3],
|
||||
[1,2],
|
||||
],
|
||||
[
|
||||
[2,4],
|
||||
[4,3]
|
||||
]
|
||||
]
|
||||
assert fn(np.array(threemixed)) == [[[3]]]
|
||||
|
||||
foursame = [
|
||||
[
|
||||
[4,4],
|
||||
[1,2],
|
||||
],
|
||||
[
|
||||
[2,4],
|
||||
[4,3]
|
||||
]
|
||||
]
|
||||
|
||||
assert fn(np.array(foursame)) == [[[4]]]
|
||||
|
||||
fivesame = [
|
||||
[
|
||||
[5,4],
|
||||
[5,5],
|
||||
],
|
||||
[
|
||||
[2,4],
|
||||
[5,5]
|
||||
]
|
||||
]
|
||||
|
||||
assert fn(np.array(fivesame)) == [[[5]]]
|
||||
|
||||
def countless3d_generalized(img):
|
||||
return countless3d.countless_generalized(img, (2,2,2))
|
||||
def countless3d_dynamic_generalized(img):
|
||||
return countless3d.dynamic_countless_generalized(img, (2,2,2))
|
||||
|
||||
methods = [
|
||||
countless3d.countless3d,
|
||||
countless3d.dynamic_countless3d,
|
||||
countless3d_generalized,
|
||||
countless3d_dynamic_generalized,
|
||||
]
|
||||
|
||||
for fn in methods:
|
||||
test_all_cases(fn)
|
||||
@@ -0,0 +1,421 @@
|
||||
import enum
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
from skimage import img_as_ubyte
|
||||
from skimage.transform import rescale, resize
|
||||
|
||||
from .countless.countless2d import zero_corrected_countless
|
||||
|
||||
|
||||
class ObjectMask():
|
||||
def __init__(self, mask):
|
||||
self.height, self.width = mask.shape
|
||||
(self.up, self.down), (self.left, self.right) = self._get_limits(mask)
|
||||
self.mask = mask[self.up:self.down, self.left:self.right].copy()
|
||||
|
||||
@staticmethod
|
||||
def _get_limits(mask):
|
||||
def indicator_limits(indicator):
|
||||
lower = indicator.argmax()
|
||||
upper = len(indicator) - indicator[::-1].argmax()
|
||||
return lower, upper
|
||||
|
||||
vertical_indicator = mask.any(axis=1)
|
||||
vertical_limits = indicator_limits(vertical_indicator)
|
||||
|
||||
horizontal_indicator = mask.any(axis=0)
|
||||
horizontal_limits = indicator_limits(horizontal_indicator)
|
||||
|
||||
return vertical_limits, horizontal_limits
|
||||
|
||||
def _clean(self):
|
||||
self.up, self.down, self.left, self.right = 0, 0, 0, 0
|
||||
self.mask = np.empty((0, 0))
|
||||
|
||||
def horizontal_flip(self, inplace=False):
|
||||
if not inplace:
|
||||
flipped = deepcopy(self)
|
||||
return flipped.horizontal_flip(inplace=True)
|
||||
|
||||
self.mask = self.mask[:, ::-1]
|
||||
return self
|
||||
|
||||
def vertical_flip(self, inplace=False):
|
||||
if not inplace:
|
||||
flipped = deepcopy(self)
|
||||
return flipped.vertical_flip(inplace=True)
|
||||
|
||||
self.mask = self.mask[::-1, :]
|
||||
return self
|
||||
|
||||
def image_center(self):
|
||||
y_center = self.up + (self.down - self.up) / 2
|
||||
x_center = self.left + (self.right - self.left) / 2
|
||||
return y_center, x_center
|
||||
|
||||
def rescale(self, scaling_factor, inplace=False):
|
||||
if not inplace:
|
||||
scaled = deepcopy(self)
|
||||
return scaled.rescale(scaling_factor, inplace=True)
|
||||
|
||||
scaled_mask = rescale(self.mask.astype(float), scaling_factor, order=0) > 0.5
|
||||
(up, down), (left, right) = self._get_limits(scaled_mask)
|
||||
self.mask = scaled_mask[up:down, left:right]
|
||||
|
||||
y_center, x_center = self.image_center()
|
||||
mask_height, mask_width = self.mask.shape
|
||||
self.up = int(round(y_center - mask_height / 2))
|
||||
self.down = self.up + mask_height
|
||||
self.left = int(round(x_center - mask_width / 2))
|
||||
self.right = self.left + mask_width
|
||||
return self
|
||||
|
||||
def crop_to_canvas(self, vertical=True, horizontal=True, inplace=False):
|
||||
if not inplace:
|
||||
cropped = deepcopy(self)
|
||||
cropped.crop_to_canvas(vertical=vertical, horizontal=horizontal, inplace=True)
|
||||
return cropped
|
||||
|
||||
if vertical:
|
||||
if self.up >= self.height or self.down <= 0:
|
||||
self._clean()
|
||||
else:
|
||||
cut_up, cut_down = max(-self.up, 0), max(self.down - self.height, 0)
|
||||
if cut_up != 0:
|
||||
self.mask = self.mask[cut_up:]
|
||||
self.up = 0
|
||||
if cut_down != 0:
|
||||
self.mask = self.mask[:-cut_down]
|
||||
self.down = self.height
|
||||
|
||||
if horizontal:
|
||||
if self.left >= self.width or self.right <= 0:
|
||||
self._clean()
|
||||
else:
|
||||
cut_left, cut_right = max(-self.left, 0), max(self.right - self.width, 0)
|
||||
if cut_left != 0:
|
||||
self.mask = self.mask[:, cut_left:]
|
||||
self.left = 0
|
||||
if cut_right != 0:
|
||||
self.mask = self.mask[:, :-cut_right]
|
||||
self.right = self.width
|
||||
|
||||
return self
|
||||
|
||||
def restore_full_mask(self, allow_crop=False):
|
||||
cropped = self.crop_to_canvas(inplace=allow_crop)
|
||||
mask = np.zeros((cropped.height, cropped.width), dtype=bool)
|
||||
mask[cropped.up:cropped.down, cropped.left:cropped.right] = cropped.mask
|
||||
return mask
|
||||
|
||||
def shift(self, vertical=0, horizontal=0, inplace=False):
|
||||
if not inplace:
|
||||
shifted = deepcopy(self)
|
||||
return shifted.shift(vertical=vertical, horizontal=horizontal, inplace=True)
|
||||
|
||||
self.up += vertical
|
||||
self.down += vertical
|
||||
self.left += horizontal
|
||||
self.right += horizontal
|
||||
return self
|
||||
|
||||
def area(self):
|
||||
return self.mask.sum()
|
||||
|
||||
|
||||
class RigidnessMode(enum.Enum):
|
||||
soft = 0
|
||||
rigid = 1
|
||||
|
||||
|
||||
class SegmentationMask:
|
||||
def __init__(self, confidence_threshold=0.5, rigidness_mode=RigidnessMode.rigid,
|
||||
max_object_area=0.3, min_mask_area=0.02, downsample_levels=6, num_variants_per_mask=4,
|
||||
max_mask_intersection=0.5, max_foreground_coverage=0.5, max_foreground_intersection=0.5,
|
||||
max_hidden_area=0.2, max_scale_change=0.25, horizontal_flip=True,
|
||||
max_vertical_shift=0.1, position_shuffle=True):
|
||||
"""
|
||||
:param confidence_threshold: float; threshold for confidence of the panoptic segmentator to allow for
|
||||
the instance.
|
||||
:param rigidness_mode: RigidnessMode object
|
||||
when soft, checks intersection only with the object from which the mask_object was produced
|
||||
when rigid, checks intersection with any foreground class object
|
||||
:param max_object_area: float; allowed upper bound for to be considered as mask_object.
|
||||
:param min_mask_area: float; lower bound for mask to be considered valid
|
||||
:param downsample_levels: int; defines width of the resized segmentation to obtain shifted masks;
|
||||
:param num_variants_per_mask: int; maximal number of the masks for the same object;
|
||||
:param max_mask_intersection: float; maximum allowed area fraction of intersection for 2 masks
|
||||
produced by horizontal shift of the same mask_object; higher value -> more diversity
|
||||
:param max_foreground_coverage: float; maximum allowed area fraction of intersection for foreground object to be
|
||||
covered by mask; lower value -> less the objects are covered
|
||||
:param max_foreground_intersection: float; maximum allowed area of intersection for the mask with foreground
|
||||
object; lower value -> mask is more on the background than on the objects
|
||||
:param max_hidden_area: upper bound on part of the object hidden by shifting object outside the screen area;
|
||||
:param max_scale_change: allowed scale change for the mask_object;
|
||||
:param horizontal_flip: if horizontal flips are allowed;
|
||||
:param max_vertical_shift: amount of vertical movement allowed;
|
||||
:param position_shuffle: shuffle
|
||||
"""
|
||||
|
||||
assert DETECTRON_INSTALLED, 'Cannot use SegmentationMask without detectron2'
|
||||
self.cfg = get_cfg()
|
||||
self.cfg.merge_from_file(model_zoo.get_config_file("COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml"))
|
||||
self.cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml")
|
||||
self.cfg.MODEL.PANOPTIC_FPN.COMBINE.INSTANCES_CONFIDENCE_THRESH = confidence_threshold
|
||||
self.predictor = DefaultPredictor(self.cfg)
|
||||
|
||||
self.rigidness_mode = RigidnessMode(rigidness_mode)
|
||||
self.max_object_area = max_object_area
|
||||
self.min_mask_area = min_mask_area
|
||||
self.downsample_levels = downsample_levels
|
||||
self.num_variants_per_mask = num_variants_per_mask
|
||||
self.max_mask_intersection = max_mask_intersection
|
||||
self.max_foreground_coverage = max_foreground_coverage
|
||||
self.max_foreground_intersection = max_foreground_intersection
|
||||
self.max_hidden_area = max_hidden_area
|
||||
self.position_shuffle = position_shuffle
|
||||
|
||||
self.max_scale_change = max_scale_change
|
||||
self.horizontal_flip = horizontal_flip
|
||||
self.max_vertical_shift = max_vertical_shift
|
||||
|
||||
def get_segmentation(self, img):
|
||||
im = img_as_ubyte(img)
|
||||
panoptic_seg, segment_info = self.predictor(im)["panoptic_seg"]
|
||||
return panoptic_seg, segment_info
|
||||
|
||||
@staticmethod
|
||||
def _is_power_of_two(n):
|
||||
return (n != 0) and (n & (n-1) == 0)
|
||||
|
||||
def identify_candidates(self, panoptic_seg, segments_info):
|
||||
potential_mask_ids = []
|
||||
for segment in segments_info:
|
||||
if not segment["isthing"]:
|
||||
continue
|
||||
mask = (panoptic_seg == segment["id"]).int().detach().cpu().numpy()
|
||||
area = mask.sum().item() / np.prod(panoptic_seg.shape)
|
||||
if area >= self.max_object_area:
|
||||
continue
|
||||
potential_mask_ids.append(segment["id"])
|
||||
return potential_mask_ids
|
||||
|
||||
def downsample_mask(self, mask):
|
||||
height, width = mask.shape
|
||||
if not (self._is_power_of_two(height) and self._is_power_of_two(width)):
|
||||
raise ValueError("Image sides are not power of 2.")
|
||||
|
||||
num_iterations = width.bit_length() - 1 - self.downsample_levels
|
||||
if num_iterations < 0:
|
||||
raise ValueError(f"Width is lower than 2^{self.downsample_levels}.")
|
||||
|
||||
if height.bit_length() - 1 < num_iterations:
|
||||
raise ValueError("Height is too low to perform downsampling")
|
||||
|
||||
downsampled = mask
|
||||
for _ in range(num_iterations):
|
||||
downsampled = zero_corrected_countless(downsampled)
|
||||
|
||||
return downsampled
|
||||
|
||||
def _augmentation_params(self):
|
||||
scaling_factor = np.random.uniform(1 - self.max_scale_change, 1 + self.max_scale_change)
|
||||
if self.horizontal_flip:
|
||||
horizontal_flip = bool(np.random.choice(2))
|
||||
else:
|
||||
horizontal_flip = False
|
||||
vertical_shift = np.random.uniform(-self.max_vertical_shift, self.max_vertical_shift)
|
||||
|
||||
return {
|
||||
"scaling_factor": scaling_factor,
|
||||
"horizontal_flip": horizontal_flip,
|
||||
"vertical_shift": vertical_shift
|
||||
}
|
||||
|
||||
def _get_intersection(self, mask_array, mask_object):
|
||||
intersection = mask_array[
|
||||
mask_object.up:mask_object.down, mask_object.left:mask_object.right
|
||||
] & mask_object.mask
|
||||
return intersection
|
||||
|
||||
def _check_masks_intersection(self, aug_mask, total_mask_area, prev_masks):
|
||||
for existing_mask in prev_masks:
|
||||
intersection_area = self._get_intersection(existing_mask, aug_mask).sum()
|
||||
intersection_existing = intersection_area / existing_mask.sum()
|
||||
intersection_current = 1 - (aug_mask.area() - intersection_area) / total_mask_area
|
||||
if (intersection_existing > self.max_mask_intersection) or \
|
||||
(intersection_current > self.max_mask_intersection):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_foreground_intersection(self, aug_mask, foreground):
|
||||
for existing_mask in foreground:
|
||||
intersection_area = self._get_intersection(existing_mask, aug_mask).sum()
|
||||
intersection_existing = intersection_area / existing_mask.sum()
|
||||
if intersection_existing > self.max_foreground_coverage:
|
||||
return False
|
||||
intersection_mask = intersection_area / aug_mask.area()
|
||||
if intersection_mask > self.max_foreground_intersection:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _move_mask(self, mask, foreground):
|
||||
# Obtaining properties of the original mask_object:
|
||||
orig_mask = ObjectMask(mask)
|
||||
|
||||
chosen_masks = []
|
||||
chosen_parameters = []
|
||||
# to fix the case when resizing gives mask_object consisting only of False
|
||||
scaling_factor_lower_bound = 0.
|
||||
|
||||
for var_idx in range(self.num_variants_per_mask):
|
||||
# Obtaining augmentation parameters and applying them to the downscaled mask_object
|
||||
augmentation_params = self._augmentation_params()
|
||||
augmentation_params["scaling_factor"] = min([
|
||||
augmentation_params["scaling_factor"],
|
||||
2 * min(orig_mask.up, orig_mask.height - orig_mask.down) / orig_mask.height + 1.,
|
||||
2 * min(orig_mask.left, orig_mask.width - orig_mask.right) / orig_mask.width + 1.
|
||||
])
|
||||
augmentation_params["scaling_factor"] = max([
|
||||
augmentation_params["scaling_factor"], scaling_factor_lower_bound
|
||||
])
|
||||
|
||||
aug_mask = deepcopy(orig_mask)
|
||||
aug_mask.rescale(augmentation_params["scaling_factor"], inplace=True)
|
||||
if augmentation_params["horizontal_flip"]:
|
||||
aug_mask.horizontal_flip(inplace=True)
|
||||
total_aug_area = aug_mask.area()
|
||||
if total_aug_area == 0:
|
||||
scaling_factor_lower_bound = 1.
|
||||
continue
|
||||
|
||||
# Fix if the element vertical shift is too strong and shown area is too small:
|
||||
vertical_area = aug_mask.mask.sum(axis=1) / total_aug_area # share of area taken by rows
|
||||
# number of rows which are allowed to be hidden from upper and lower parts of image respectively
|
||||
max_hidden_up = np.searchsorted(vertical_area.cumsum(), self.max_hidden_area)
|
||||
max_hidden_down = np.searchsorted(vertical_area[::-1].cumsum(), self.max_hidden_area)
|
||||
# correcting vertical shift, so not too much area will be hidden
|
||||
augmentation_params["vertical_shift"] = np.clip(
|
||||
augmentation_params["vertical_shift"],
|
||||
-(aug_mask.up + max_hidden_up) / aug_mask.height,
|
||||
(aug_mask.height - aug_mask.down + max_hidden_down) / aug_mask.height
|
||||
)
|
||||
# Applying vertical shift:
|
||||
vertical_shift = int(round(aug_mask.height * augmentation_params["vertical_shift"]))
|
||||
aug_mask.shift(vertical=vertical_shift, inplace=True)
|
||||
aug_mask.crop_to_canvas(vertical=True, horizontal=False, inplace=True)
|
||||
|
||||
# Choosing horizontal shift:
|
||||
max_hidden_area = self.max_hidden_area - (1 - aug_mask.area() / total_aug_area)
|
||||
horizontal_area = aug_mask.mask.sum(axis=0) / total_aug_area
|
||||
max_hidden_left = np.searchsorted(horizontal_area.cumsum(), max_hidden_area)
|
||||
max_hidden_right = np.searchsorted(horizontal_area[::-1].cumsum(), max_hidden_area)
|
||||
allowed_shifts = np.arange(-max_hidden_left, aug_mask.width -
|
||||
(aug_mask.right - aug_mask.left) + max_hidden_right + 1)
|
||||
allowed_shifts = - (aug_mask.left - allowed_shifts)
|
||||
|
||||
if self.position_shuffle:
|
||||
np.random.shuffle(allowed_shifts)
|
||||
|
||||
mask_is_found = False
|
||||
for horizontal_shift in allowed_shifts:
|
||||
aug_mask_left = deepcopy(aug_mask)
|
||||
aug_mask_left.shift(horizontal=horizontal_shift, inplace=True)
|
||||
aug_mask_left.crop_to_canvas(inplace=True)
|
||||
|
||||
prev_masks = [mask] + chosen_masks
|
||||
is_mask_suitable = self._check_masks_intersection(aug_mask_left, total_aug_area, prev_masks) & \
|
||||
self._check_foreground_intersection(aug_mask_left, foreground)
|
||||
if is_mask_suitable:
|
||||
aug_draw = aug_mask_left.restore_full_mask()
|
||||
chosen_masks.append(aug_draw)
|
||||
augmentation_params["horizontal_shift"] = horizontal_shift / aug_mask_left.width
|
||||
chosen_parameters.append(augmentation_params)
|
||||
mask_is_found = True
|
||||
break
|
||||
|
||||
if not mask_is_found:
|
||||
break
|
||||
|
||||
return chosen_parameters
|
||||
|
||||
def _prepare_mask(self, mask):
|
||||
height, width = mask.shape
|
||||
target_width = width if self._is_power_of_two(width) else (1 << width.bit_length())
|
||||
target_height = height if self._is_power_of_two(height) else (1 << height.bit_length())
|
||||
|
||||
return resize(mask.astype('float32'), (target_height, target_width), order=0, mode='edge').round().astype('int32')
|
||||
|
||||
def get_masks(self, im, return_panoptic=False):
|
||||
panoptic_seg, segments_info = self.get_segmentation(im)
|
||||
potential_mask_ids = self.identify_candidates(panoptic_seg, segments_info)
|
||||
|
||||
panoptic_seg_scaled = self._prepare_mask(panoptic_seg.detach().cpu().numpy())
|
||||
downsampled = self.downsample_mask(panoptic_seg_scaled)
|
||||
scene_objects = []
|
||||
for segment in segments_info:
|
||||
if not segment["isthing"]:
|
||||
continue
|
||||
mask = downsampled == segment["id"]
|
||||
if not np.any(mask):
|
||||
continue
|
||||
scene_objects.append(mask)
|
||||
|
||||
mask_set = []
|
||||
for mask_id in potential_mask_ids:
|
||||
mask = downsampled == mask_id
|
||||
if not np.any(mask):
|
||||
continue
|
||||
|
||||
if self.rigidness_mode is RigidnessMode.soft:
|
||||
foreground = [mask]
|
||||
elif self.rigidness_mode is RigidnessMode.rigid:
|
||||
foreground = scene_objects
|
||||
else:
|
||||
raise ValueError(f'Unexpected rigidness_mode: {rigidness_mode}')
|
||||
|
||||
masks_params = self._move_mask(mask, foreground)
|
||||
|
||||
full_mask = ObjectMask((panoptic_seg == mask_id).detach().cpu().numpy())
|
||||
|
||||
for params in masks_params:
|
||||
aug_mask = deepcopy(full_mask)
|
||||
aug_mask.rescale(params["scaling_factor"], inplace=True)
|
||||
if params["horizontal_flip"]:
|
||||
aug_mask.horizontal_flip(inplace=True)
|
||||
|
||||
vertical_shift = int(round(aug_mask.height * params["vertical_shift"]))
|
||||
horizontal_shift = int(round(aug_mask.width * params["horizontal_shift"]))
|
||||
aug_mask.shift(vertical=vertical_shift, horizontal=horizontal_shift, inplace=True)
|
||||
aug_mask = aug_mask.restore_full_mask().astype('uint8')
|
||||
if aug_mask.mean() <= self.min_mask_area:
|
||||
continue
|
||||
mask_set.append(aug_mask)
|
||||
|
||||
if return_panoptic:
|
||||
return mask_set, panoptic_seg.detach().cpu().numpy()
|
||||
else:
|
||||
return mask_set
|
||||
|
||||
|
||||
def propose_random_square_crop(mask, min_overlap=0.5):
|
||||
height, width = mask.shape
|
||||
mask_ys, mask_xs = np.where(mask > 0.5) # mask==0 is known fragment and mask==1 is missing
|
||||
|
||||
if height < width:
|
||||
crop_size = height
|
||||
obj_left, obj_right = mask_xs.min(), mask_xs.max()
|
||||
obj_width = obj_right - obj_left
|
||||
left_border = max(0, min(width - crop_size - 1, obj_left + obj_width * min_overlap - crop_size))
|
||||
right_border = max(left_border + 1, min(width - crop_size, obj_left + obj_width * min_overlap))
|
||||
start_x = np.random.randint(left_border, right_border)
|
||||
return start_x, 0, start_x + crop_size, height
|
||||
else:
|
||||
crop_size = width
|
||||
obj_top, obj_bottom = mask_ys.min(), mask_ys.max()
|
||||
obj_height = obj_bottom - obj_top
|
||||
top_border = max(0, min(height - crop_size - 1, obj_top + obj_height * min_overlap - crop_size))
|
||||
bottom_border = max(top_border + 1, min(height - crop_size, obj_top + obj_height * min_overlap))
|
||||
start_y = np.random.randint(top_border, bottom_border)
|
||||
return 0, start_y, width, start_y + crop_size
|
||||
@@ -0,0 +1,314 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.optim import Adam, SGD
|
||||
from kornia.filters import gaussian_blur2d
|
||||
from kornia.geometry.transform import resize
|
||||
from kornia.morphology import erosion
|
||||
from torch.nn import functional as F
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
from saicinpainting.evaluation.data import pad_tensor_to_modulo
|
||||
from saicinpainting.evaluation.utils import move_to_device
|
||||
from saicinpainting.training.modules.ffc import FFCResnetBlock
|
||||
from saicinpainting.training.modules.pix2pixhd import ResnetBlock
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def _pyrdown(im : torch.Tensor, downsize : tuple=None):
|
||||
"""downscale the image"""
|
||||
if downsize is None:
|
||||
downsize = (im.shape[2]//2, im.shape[3]//2)
|
||||
assert im.shape[1] == 3, "Expected shape for the input to be (n,3,height,width)"
|
||||
im = gaussian_blur2d(im, kernel_size=(5,5), sigma=(1.0,1.0))
|
||||
im = F.interpolate(im, size=downsize, mode='bilinear', align_corners=False)
|
||||
return im
|
||||
|
||||
def _pyrdown_mask(mask : torch.Tensor, downsize : tuple=None, eps : float=1e-8, blur_mask : bool=True, round_up : bool=True):
|
||||
"""downscale the mask tensor
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mask : torch.Tensor
|
||||
mask of size (B, 1, H, W)
|
||||
downsize : tuple, optional
|
||||
size to downscale to. If None, image is downscaled to half, by default None
|
||||
eps : float, optional
|
||||
threshold value for binarizing the mask, by default 1e-8
|
||||
blur_mask : bool, optional
|
||||
if True, apply gaussian filter before downscaling, by default True
|
||||
round_up : bool, optional
|
||||
if True, values above eps are marked 1, else, values below 1-eps are marked 0, by default True
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
downscaled mask
|
||||
"""
|
||||
|
||||
if downsize is None:
|
||||
downsize = (mask.shape[2]//2, mask.shape[3]//2)
|
||||
assert mask.shape[1] == 1, "Expected shape for the input to be (n,1,height,width)"
|
||||
if blur_mask == True:
|
||||
mask = gaussian_blur2d(mask, kernel_size=(5,5), sigma=(1.0,1.0))
|
||||
mask = F.interpolate(mask, size=downsize, mode='bilinear', align_corners=False)
|
||||
else:
|
||||
mask = F.interpolate(mask, size=downsize, mode='bilinear', align_corners=False)
|
||||
if round_up:
|
||||
mask[mask>=eps] = 1
|
||||
mask[mask<eps] = 0
|
||||
else:
|
||||
mask[mask>=1.0-eps] = 1
|
||||
mask[mask<1.0-eps] = 0
|
||||
return mask
|
||||
|
||||
def _erode_mask(mask : torch.Tensor, ekernel : torch.Tensor=None, eps : float=1e-8):
|
||||
"""erode the mask, and set gray pixels to 0"""
|
||||
if ekernel is not None:
|
||||
mask = erosion(mask, ekernel)
|
||||
mask[mask>=1.0-eps] = 1
|
||||
mask[mask<1.0-eps] = 0
|
||||
return mask
|
||||
|
||||
|
||||
def _l1_loss(
|
||||
pred : torch.Tensor, pred_downscaled : torch.Tensor, ref : torch.Tensor,
|
||||
mask : torch.Tensor, mask_downscaled : torch.Tensor,
|
||||
image : torch.Tensor, on_pred : bool=True
|
||||
):
|
||||
"""l1 loss on src pixels, and downscaled predictions if on_pred=True"""
|
||||
loss = torch.mean(torch.abs(pred[mask<1e-8] - image[mask<1e-8]))
|
||||
if on_pred:
|
||||
loss += torch.mean(torch.abs(pred_downscaled[mask_downscaled>=1e-8] - ref[mask_downscaled>=1e-8]))
|
||||
return loss
|
||||
|
||||
def _infer(
|
||||
image : torch.Tensor, mask : torch.Tensor,
|
||||
forward_front : nn.Module, forward_rears : nn.Module,
|
||||
ref_lower_res : torch.Tensor, orig_shape : tuple, devices : list,
|
||||
scale_ind : int, n_iters : int=15, lr : float=0.002):
|
||||
"""Performs inference with refinement at a given scale.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : torch.Tensor
|
||||
input image to be inpainted, of size (1,3,H,W)
|
||||
mask : torch.Tensor
|
||||
input inpainting mask, of size (1,1,H,W)
|
||||
forward_front : nn.Module
|
||||
the front part of the inpainting network
|
||||
forward_rears : nn.Module
|
||||
the rear part of the inpainting network
|
||||
ref_lower_res : torch.Tensor
|
||||
the inpainting at previous scale, used as reference image
|
||||
orig_shape : tuple
|
||||
shape of the original input image before padding
|
||||
devices : list
|
||||
list of available devices
|
||||
scale_ind : int
|
||||
the scale index
|
||||
n_iters : int, optional
|
||||
number of iterations of refinement, by default 15
|
||||
lr : float, optional
|
||||
learning rate, by default 0.002
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
inpainted image
|
||||
"""
|
||||
masked_image = image * (1 - mask)
|
||||
masked_image = torch.cat([masked_image, mask], dim=1)
|
||||
|
||||
mask = mask.repeat(1,3,1,1)
|
||||
if ref_lower_res is not None:
|
||||
ref_lower_res = ref_lower_res.detach()
|
||||
with torch.no_grad():
|
||||
z1,z2 = forward_front(masked_image)
|
||||
# Inference
|
||||
mask = mask.to(devices[-1])
|
||||
ekernel = torch.from_numpy(cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(15,15)).astype(bool)).float()
|
||||
ekernel = ekernel.to(devices[-1])
|
||||
image = image.to(devices[-1])
|
||||
z1, z2 = z1.detach().to(devices[0]), z2.detach().to(devices[0])
|
||||
z1.requires_grad, z2.requires_grad = True, True
|
||||
|
||||
optimizer = Adam([z1,z2], lr=lr)
|
||||
|
||||
pbar = tqdm(range(n_iters), leave=False)
|
||||
for idi in pbar:
|
||||
optimizer.zero_grad()
|
||||
input_feat = (z1,z2)
|
||||
for idd, forward_rear in enumerate(forward_rears):
|
||||
output_feat = forward_rear(input_feat)
|
||||
if idd < len(devices) - 1:
|
||||
midz1, midz2 = output_feat
|
||||
midz1, midz2 = midz1.to(devices[idd+1]), midz2.to(devices[idd+1])
|
||||
input_feat = (midz1, midz2)
|
||||
else:
|
||||
pred = output_feat
|
||||
|
||||
if ref_lower_res is None:
|
||||
break
|
||||
losses = {}
|
||||
######################### multi-scale #############################
|
||||
# scaled loss with downsampler
|
||||
pred_downscaled = _pyrdown(pred[:,:,:orig_shape[0],:orig_shape[1]])
|
||||
mask_downscaled = _pyrdown_mask(mask[:,:1,:orig_shape[0],:orig_shape[1]], blur_mask=False, round_up=False)
|
||||
mask_downscaled = _erode_mask(mask_downscaled, ekernel=ekernel)
|
||||
mask_downscaled = mask_downscaled.repeat(1,3,1,1)
|
||||
losses["ms_l1"] = _l1_loss(pred, pred_downscaled, ref_lower_res, mask, mask_downscaled, image, on_pred=True)
|
||||
|
||||
loss = sum(losses.values())
|
||||
pbar.set_description("Refining scale {} using scale {} ...current loss: {:.4f}".format(scale_ind+1, scale_ind, loss.item()))
|
||||
if idi < n_iters - 1:
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
del pred_downscaled
|
||||
del loss
|
||||
del pred
|
||||
# "pred" is the prediction after Plug-n-Play module
|
||||
inpainted = mask * pred + (1 - mask) * image
|
||||
inpainted = inpainted.detach().cpu()
|
||||
return inpainted
|
||||
|
||||
def _get_image_mask_pyramid(batch : dict, min_side : int, max_scales : int, px_budget : int):
|
||||
"""Build the image mask pyramid
|
||||
|
||||
Parameters
|
||||
----------
|
||||
batch : dict
|
||||
batch containing image, mask, etc
|
||||
min_side : int
|
||||
minimum side length to limit the number of scales of the pyramid
|
||||
max_scales : int
|
||||
maximum number of scales allowed
|
||||
px_budget : int
|
||||
the product H*W cannot exceed this budget, because of resource constraints
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple
|
||||
image-mask pyramid in the form of list of images and list of masks
|
||||
"""
|
||||
|
||||
assert batch['image'].shape[0] == 1, "refiner works on only batches of size 1!"
|
||||
|
||||
h, w = batch['unpad_to_size']
|
||||
h, w = h[0].item(), w[0].item()
|
||||
|
||||
image = batch['image'][...,:h,:w]
|
||||
mask = batch['mask'][...,:h,:w]
|
||||
if h*w > px_budget:
|
||||
#resize
|
||||
ratio = np.sqrt(px_budget / float(h*w))
|
||||
h_orig, w_orig = h, w
|
||||
h,w = int(h*ratio), int(w*ratio)
|
||||
print(f"Original image too large for refinement! Resizing {(h_orig,w_orig)} to {(h,w)}...")
|
||||
image = resize(image, (h,w),interpolation='bilinear', align_corners=False)
|
||||
mask = resize(mask, (h,w),interpolation='bilinear', align_corners=False)
|
||||
mask[mask>1e-8] = 1
|
||||
breadth = min(h,w)
|
||||
n_scales = min(1 + int(round(max(0,np.log2(breadth / min_side)))), max_scales)
|
||||
ls_images = []
|
||||
ls_masks = []
|
||||
|
||||
ls_images.append(image)
|
||||
ls_masks.append(mask)
|
||||
|
||||
for _ in range(n_scales - 1):
|
||||
image_p = _pyrdown(ls_images[-1])
|
||||
mask_p = _pyrdown_mask(ls_masks[-1])
|
||||
ls_images.append(image_p)
|
||||
ls_masks.append(mask_p)
|
||||
# reverse the lists because we want the lowest resolution image as index 0
|
||||
return ls_images[::-1], ls_masks[::-1]
|
||||
|
||||
def refine_predict(
|
||||
batch : dict, inpainter : nn.Module, gpu_ids : str,
|
||||
modulo : int, n_iters : int, lr : float, min_side : int,
|
||||
max_scales : int, px_budget : int
|
||||
):
|
||||
"""Refines the inpainting of the network
|
||||
|
||||
Parameters
|
||||
----------
|
||||
batch : dict
|
||||
image-mask batch, currently we assume the batchsize to be 1
|
||||
inpainter : nn.Module
|
||||
the inpainting neural network
|
||||
gpu_ids : str
|
||||
the GPU ids of the machine to use. If only single GPU, use: "0,"
|
||||
modulo : int
|
||||
pad the image to ensure dimension % modulo == 0
|
||||
n_iters : int
|
||||
number of iterations of refinement for each scale
|
||||
lr : float
|
||||
learning rate
|
||||
min_side : int
|
||||
all sides of image on all scales should be >= min_side / sqrt(2)
|
||||
max_scales : int
|
||||
max number of downscaling scales for the image-mask pyramid
|
||||
px_budget : int
|
||||
pixels budget. Any image will be resized to satisfy height*width <= px_budget
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
inpainted image of size (1,3,H,W)
|
||||
"""
|
||||
|
||||
assert not inpainter.training
|
||||
assert not inpainter.add_noise_kwargs
|
||||
assert inpainter.concat_mask
|
||||
|
||||
gpu_ids = [f'cuda:{gpuid}' for gpuid in gpu_ids.replace(" ","").split(",") if gpuid.isdigit()]
|
||||
n_resnet_blocks = 0
|
||||
first_resblock_ind = 0
|
||||
found_first_resblock = False
|
||||
for idl in range(len(inpainter.generator.model)):
|
||||
if isinstance(inpainter.generator.model[idl], FFCResnetBlock) or isinstance(inpainter.generator.model[idl], ResnetBlock):
|
||||
n_resnet_blocks += 1
|
||||
found_first_resblock = True
|
||||
elif not found_first_resblock:
|
||||
first_resblock_ind += 1
|
||||
resblocks_per_gpu = n_resnet_blocks // len(gpu_ids)
|
||||
|
||||
devices = [torch.device(gpu_id) for gpu_id in gpu_ids]
|
||||
|
||||
# split the model into front, and rear parts
|
||||
forward_front = inpainter.generator.model[0:first_resblock_ind]
|
||||
forward_front.to(devices[0])
|
||||
forward_rears = []
|
||||
for idd in range(len(gpu_ids)):
|
||||
if idd < len(gpu_ids) - 1:
|
||||
forward_rears.append(inpainter.generator.model[first_resblock_ind + resblocks_per_gpu*(idd):first_resblock_ind+resblocks_per_gpu*(idd+1)])
|
||||
else:
|
||||
forward_rears.append(inpainter.generator.model[first_resblock_ind + resblocks_per_gpu*(idd):])
|
||||
forward_rears[idd].to(devices[idd])
|
||||
|
||||
ls_images, ls_masks = _get_image_mask_pyramid(
|
||||
batch,
|
||||
min_side,
|
||||
max_scales,
|
||||
px_budget
|
||||
)
|
||||
image_inpainted = None
|
||||
|
||||
for ids, (image, mask) in enumerate(zip(ls_images, ls_masks)):
|
||||
orig_shape = image.shape[2:]
|
||||
image = pad_tensor_to_modulo(image, modulo)
|
||||
mask = pad_tensor_to_modulo(mask, modulo)
|
||||
mask[mask >= 1e-8] = 1.0
|
||||
mask[mask < 1e-8] = 0.0
|
||||
image, mask = move_to_device(image, devices[0]), move_to_device(mask, devices[0])
|
||||
if image_inpainted is not None:
|
||||
image_inpainted = move_to_device(image_inpainted, devices[-1])
|
||||
image_inpainted = _infer(image, mask, forward_front, forward_rears, image_inpainted, orig_shape, devices, ids, n_iters, lr)
|
||||
image_inpainted = image_inpainted[:,:,:orig_shape[0], :orig_shape[1]]
|
||||
# detach everything to save resources
|
||||
image = image.detach().cpu()
|
||||
mask = mask.detach().cpu()
|
||||
|
||||
return image_inpainted
|
||||
@@ -0,0 +1,28 @@
|
||||
from enum import Enum
|
||||
|
||||
import yaml
|
||||
from easydict import EasyDict as edict
|
||||
import torch.nn as nn
|
||||
import torch
|
||||
|
||||
|
||||
def load_yaml(path):
|
||||
with open(path, 'r') as f:
|
||||
return edict(yaml.safe_load(f))
|
||||
|
||||
|
||||
def move_to_device(obj, device):
|
||||
if isinstance(obj, nn.Module):
|
||||
return obj.to(device)
|
||||
if torch.is_tensor(obj):
|
||||
return obj.to(device)
|
||||
if isinstance(obj, (tuple, list)):
|
||||
return [move_to_device(el, device) for el in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {name: move_to_device(val, device) for name, val in obj.items()}
|
||||
raise ValueError(f'Unexpected type {type(obj)}')
|
||||
|
||||
|
||||
class SmallMode(Enum):
|
||||
DROP = "drop"
|
||||
UPSCALE = "upscale"
|
||||
@@ -0,0 +1,37 @@
|
||||
import numpy as np
|
||||
from skimage import io
|
||||
from skimage.segmentation import mark_boundaries
|
||||
|
||||
|
||||
def save_item_for_vis(item, out_file):
|
||||
mask = item['mask'] > 0.5
|
||||
if mask.ndim == 3:
|
||||
mask = mask[0]
|
||||
img = mark_boundaries(np.transpose(item['image'], (1, 2, 0)),
|
||||
mask,
|
||||
color=(1., 0., 0.),
|
||||
outline_color=(1., 1., 1.),
|
||||
mode='thick')
|
||||
|
||||
if 'inpainted' in item:
|
||||
inp_img = mark_boundaries(np.transpose(item['inpainted'], (1, 2, 0)),
|
||||
mask,
|
||||
color=(1., 0., 0.),
|
||||
mode='outer')
|
||||
img = np.concatenate((img, inp_img), axis=1)
|
||||
|
||||
img = np.clip(img * 255, 0, 255).astype('uint8')
|
||||
io.imsave(out_file, img)
|
||||
|
||||
|
||||
def save_mask_for_sidebyside(item, out_file):
|
||||
mask = item['mask']# > 0.5
|
||||
if mask.ndim == 3:
|
||||
mask = mask[0]
|
||||
mask = np.clip(mask * 255, 0, 255).astype('uint8')
|
||||
io.imsave(out_file, mask)
|
||||
|
||||
def save_img_for_sidebyside(item, out_file):
|
||||
img = np.transpose(item['image'], (1, 2, 0))
|
||||
img = np.clip(img * 255, 0, 255).astype('uint8')
|
||||
io.imsave(out_file, img)
|
||||
@@ -0,0 +1,84 @@
|
||||
from albumentations import DualIAATransform, to_tuple
|
||||
import imgaug.augmenters as iaa
|
||||
|
||||
class IAAAffine2(DualIAATransform):
|
||||
"""Place a regular grid of points on the input and randomly move the neighbourhood of these point around
|
||||
via affine transformations.
|
||||
|
||||
Note: This class introduce interpolation artifacts to mask if it has values other than {0;1}
|
||||
|
||||
Args:
|
||||
p (float): probability of applying the transform. Default: 0.5.
|
||||
|
||||
Targets:
|
||||
image, mask
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scale=(0.7, 1.3),
|
||||
translate_percent=None,
|
||||
translate_px=None,
|
||||
rotate=0.0,
|
||||
shear=(-0.1, 0.1),
|
||||
order=1,
|
||||
cval=0,
|
||||
mode="reflect",
|
||||
always_apply=False,
|
||||
p=0.5,
|
||||
):
|
||||
super(IAAAffine2, self).__init__(always_apply, p)
|
||||
self.scale = dict(x=scale, y=scale)
|
||||
self.translate_percent = to_tuple(translate_percent, 0)
|
||||
self.translate_px = to_tuple(translate_px, 0)
|
||||
self.rotate = to_tuple(rotate)
|
||||
self.shear = dict(x=shear, y=shear)
|
||||
self.order = order
|
||||
self.cval = cval
|
||||
self.mode = mode
|
||||
|
||||
@property
|
||||
def processor(self):
|
||||
return iaa.Affine(
|
||||
self.scale,
|
||||
self.translate_percent,
|
||||
self.translate_px,
|
||||
self.rotate,
|
||||
self.shear,
|
||||
self.order,
|
||||
self.cval,
|
||||
self.mode,
|
||||
)
|
||||
|
||||
def get_transform_init_args_names(self):
|
||||
return ("scale", "translate_percent", "translate_px", "rotate", "shear", "order", "cval", "mode")
|
||||
|
||||
|
||||
class IAAPerspective2(DualIAATransform):
|
||||
"""Perform a random four point perspective transform of the input.
|
||||
|
||||
Note: This class introduce interpolation artifacts to mask if it has values other than {0;1}
|
||||
|
||||
Args:
|
||||
scale ((float, float): standard deviation of the normal distributions. These are used to sample
|
||||
the random distances of the subimage's corners from the full image's corners. Default: (0.05, 0.1).
|
||||
p (float): probability of applying the transform. Default: 0.5.
|
||||
|
||||
Targets:
|
||||
image, mask
|
||||
"""
|
||||
|
||||
def __init__(self, scale=(0.05, 0.1), keep_size=True, always_apply=False, p=0.5,
|
||||
order=1, cval=0, mode="replicate"):
|
||||
super(IAAPerspective2, self).__init__(always_apply, p)
|
||||
self.scale = to_tuple(scale, 1.0)
|
||||
self.keep_size = keep_size
|
||||
self.cval = cval
|
||||
self.mode = mode
|
||||
|
||||
@property
|
||||
def processor(self):
|
||||
return iaa.PerspectiveTransform(self.scale, keep_size=self.keep_size, mode=self.mode, cval=self.cval)
|
||||
|
||||
def get_transform_init_args_names(self):
|
||||
return ("scale", "keep_size")
|
||||
@@ -0,0 +1,305 @@
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
|
||||
import albumentations as A
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import webdataset
|
||||
from omegaconf import open_dict, OmegaConf
|
||||
from skimage.feature import canny
|
||||
from skimage.transform import rescale, resize
|
||||
from torch.utils.data import Dataset, IterableDataset, DataLoader, DistributedSampler, ConcatDataset
|
||||
|
||||
from saicinpainting.evaluation.data import InpaintingDataset as InpaintingEvaluationDataset
|
||||
from saicinpainting.evaluation.data import OurInpaintingDataset as OurInpaintingEvaluationDataset
|
||||
from saicinpainting.evaluation.data import ceil_modulo, InpaintingEvalOnlineDataset
|
||||
from saicinpainting.training.data.aug import IAAAffine2, IAAPerspective2
|
||||
from saicinpainting.training.data.masks import get_mask_generator
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InpaintingTrainDataset(Dataset):
|
||||
def __init__(self, indir, mask_generator, transform):
|
||||
self.in_files = list(glob.glob(os.path.join(indir, '**', '*.jpg'), recursive=True))
|
||||
self.mask_generator = mask_generator
|
||||
self.transform = transform
|
||||
self.iter_i = 0
|
||||
|
||||
def __len__(self):
|
||||
return len(self.in_files)
|
||||
|
||||
def __getitem__(self, item):
|
||||
path = self.in_files[item]
|
||||
img = cv2.imread(path)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = self.transform(image=img)['image']
|
||||
img = np.transpose(img, (2, 0, 1))
|
||||
# TODO: maybe generate mask before augmentations? slower, but better for segmentation-based masks
|
||||
mask = self.mask_generator(img, iter_i=self.iter_i)
|
||||
self.iter_i += 1
|
||||
return dict(image=img,
|
||||
mask=mask)
|
||||
|
||||
|
||||
class InpaintingTrainWebDataset(IterableDataset):
|
||||
def __init__(self, indir, mask_generator, transform, shuffle_buffer=200):
|
||||
self.impl = webdataset.Dataset(indir).shuffle(shuffle_buffer).decode('rgb').to_tuple('jpg')
|
||||
self.mask_generator = mask_generator
|
||||
self.transform = transform
|
||||
|
||||
def __iter__(self):
|
||||
for iter_i, (img,) in enumerate(self.impl):
|
||||
img = np.clip(img * 255, 0, 255).astype('uint8')
|
||||
img = self.transform(image=img)['image']
|
||||
img = np.transpose(img, (2, 0, 1))
|
||||
mask = self.mask_generator(img, iter_i=iter_i)
|
||||
yield dict(image=img,
|
||||
mask=mask)
|
||||
|
||||
|
||||
class ImgSegmentationDataset(Dataset):
|
||||
def __init__(self, indir, mask_generator, transform, out_size, segm_indir, semantic_seg_n_classes):
|
||||
self.indir = indir
|
||||
self.segm_indir = segm_indir
|
||||
self.mask_generator = mask_generator
|
||||
self.transform = transform
|
||||
self.out_size = out_size
|
||||
self.semantic_seg_n_classes = semantic_seg_n_classes
|
||||
self.in_files = list(glob.glob(os.path.join(indir, '**', '*.jpg'), recursive=True))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.in_files)
|
||||
|
||||
def __getitem__(self, item):
|
||||
path = self.in_files[item]
|
||||
img = cv2.imread(path)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = cv2.resize(img, (self.out_size, self.out_size))
|
||||
img = self.transform(image=img)['image']
|
||||
img = np.transpose(img, (2, 0, 1))
|
||||
mask = self.mask_generator(img)
|
||||
segm, segm_classes= self.load_semantic_segm(path)
|
||||
result = dict(image=img,
|
||||
mask=mask,
|
||||
segm=segm,
|
||||
segm_classes=segm_classes)
|
||||
return result
|
||||
|
||||
def load_semantic_segm(self, img_path):
|
||||
segm_path = img_path.replace(self.indir, self.segm_indir).replace(".jpg", ".png")
|
||||
mask = cv2.imread(segm_path, cv2.IMREAD_GRAYSCALE)
|
||||
mask = cv2.resize(mask, (self.out_size, self.out_size))
|
||||
tensor = torch.from_numpy(np.clip(mask.astype(int)-1, 0, None))
|
||||
ohe = F.one_hot(tensor.long(), num_classes=self.semantic_seg_n_classes) # w x h x n_classes
|
||||
return ohe.permute(2, 0, 1).float(), tensor.unsqueeze(0)
|
||||
|
||||
|
||||
def get_transforms(transform_variant, out_size):
|
||||
if transform_variant == 'default':
|
||||
transform = A.Compose([
|
||||
A.RandomScale(scale_limit=0.2), # +/- 20%
|
||||
A.PadIfNeeded(min_height=out_size, min_width=out_size),
|
||||
A.RandomCrop(height=out_size, width=out_size),
|
||||
A.HorizontalFlip(),
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'distortions':
|
||||
transform = A.Compose([
|
||||
IAAPerspective2(scale=(0.0, 0.06)),
|
||||
IAAAffine2(scale=(0.7, 1.3),
|
||||
rotate=(-40, 40),
|
||||
shear=(-0.1, 0.1)),
|
||||
A.PadIfNeeded(min_height=out_size, min_width=out_size),
|
||||
A.OpticalDistortion(),
|
||||
A.RandomCrop(height=out_size, width=out_size),
|
||||
A.HorizontalFlip(),
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'distortions_scale05_1':
|
||||
transform = A.Compose([
|
||||
IAAPerspective2(scale=(0.0, 0.06)),
|
||||
IAAAffine2(scale=(0.5, 1.0),
|
||||
rotate=(-40, 40),
|
||||
shear=(-0.1, 0.1),
|
||||
p=1),
|
||||
A.PadIfNeeded(min_height=out_size, min_width=out_size),
|
||||
A.OpticalDistortion(),
|
||||
A.RandomCrop(height=out_size, width=out_size),
|
||||
A.HorizontalFlip(),
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'distortions_scale03_12':
|
||||
transform = A.Compose([
|
||||
IAAPerspective2(scale=(0.0, 0.06)),
|
||||
IAAAffine2(scale=(0.3, 1.2),
|
||||
rotate=(-40, 40),
|
||||
shear=(-0.1, 0.1),
|
||||
p=1),
|
||||
A.PadIfNeeded(min_height=out_size, min_width=out_size),
|
||||
A.OpticalDistortion(),
|
||||
A.RandomCrop(height=out_size, width=out_size),
|
||||
A.HorizontalFlip(),
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'distortions_scale03_07':
|
||||
transform = A.Compose([
|
||||
IAAPerspective2(scale=(0.0, 0.06)),
|
||||
IAAAffine2(scale=(0.3, 0.7), # scale 512 to 256 in average
|
||||
rotate=(-40, 40),
|
||||
shear=(-0.1, 0.1),
|
||||
p=1),
|
||||
A.PadIfNeeded(min_height=out_size, min_width=out_size),
|
||||
A.OpticalDistortion(),
|
||||
A.RandomCrop(height=out_size, width=out_size),
|
||||
A.HorizontalFlip(),
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'distortions_light':
|
||||
transform = A.Compose([
|
||||
IAAPerspective2(scale=(0.0, 0.02)),
|
||||
IAAAffine2(scale=(0.8, 1.8),
|
||||
rotate=(-20, 20),
|
||||
shear=(-0.03, 0.03)),
|
||||
A.PadIfNeeded(min_height=out_size, min_width=out_size),
|
||||
A.RandomCrop(height=out_size, width=out_size),
|
||||
A.HorizontalFlip(),
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'non_space_transform':
|
||||
transform = A.Compose([
|
||||
A.CLAHE(),
|
||||
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2),
|
||||
A.HueSaturationValue(hue_shift_limit=5, sat_shift_limit=30, val_shift_limit=5),
|
||||
A.ToFloat()
|
||||
])
|
||||
elif transform_variant == 'no_augs':
|
||||
transform = A.Compose([
|
||||
A.ToFloat()
|
||||
])
|
||||
else:
|
||||
raise ValueError(f'Unexpected transform_variant {transform_variant}')
|
||||
return transform
|
||||
|
||||
|
||||
def make_default_train_dataloader(indir, kind='default', out_size=512, mask_gen_kwargs=None, transform_variant='default',
|
||||
mask_generator_kind="mixed", dataloader_kwargs=None, ddp_kwargs=None, **kwargs):
|
||||
LOGGER.info(f'Make train dataloader {kind} from {indir}. Using mask generator={mask_generator_kind}')
|
||||
|
||||
mask_generator = get_mask_generator(kind=mask_generator_kind, kwargs=mask_gen_kwargs)
|
||||
transform = get_transforms(transform_variant, out_size)
|
||||
|
||||
if kind == 'default':
|
||||
dataset = InpaintingTrainDataset(indir=indir,
|
||||
mask_generator=mask_generator,
|
||||
transform=transform,
|
||||
**kwargs)
|
||||
elif kind == 'default_web':
|
||||
dataset = InpaintingTrainWebDataset(indir=indir,
|
||||
mask_generator=mask_generator,
|
||||
transform=transform,
|
||||
**kwargs)
|
||||
elif kind == 'img_with_segm':
|
||||
dataset = ImgSegmentationDataset(indir=indir,
|
||||
mask_generator=mask_generator,
|
||||
transform=transform,
|
||||
out_size=out_size,
|
||||
**kwargs)
|
||||
else:
|
||||
raise ValueError(f'Unknown train dataset kind {kind}')
|
||||
|
||||
if dataloader_kwargs is None:
|
||||
dataloader_kwargs = {}
|
||||
|
||||
is_dataset_only_iterable = kind in ('default_web',)
|
||||
|
||||
if ddp_kwargs is not None and not is_dataset_only_iterable:
|
||||
dataloader_kwargs['shuffle'] = False
|
||||
dataloader_kwargs['sampler'] = DistributedSampler(dataset, **ddp_kwargs)
|
||||
|
||||
if is_dataset_only_iterable and 'shuffle' in dataloader_kwargs:
|
||||
with open_dict(dataloader_kwargs):
|
||||
del dataloader_kwargs['shuffle']
|
||||
|
||||
dataloader = DataLoader(dataset, **dataloader_kwargs)
|
||||
return dataloader
|
||||
|
||||
|
||||
def make_default_val_dataset(indir, kind='default', out_size=512, transform_variant='default', **kwargs):
|
||||
if OmegaConf.is_list(indir) or isinstance(indir, (tuple, list)):
|
||||
return ConcatDataset([
|
||||
make_default_val_dataset(idir, kind=kind, out_size=out_size, transform_variant=transform_variant, **kwargs) for idir in indir
|
||||
])
|
||||
|
||||
LOGGER.info(f'Make val dataloader {kind} from {indir}')
|
||||
mask_generator = get_mask_generator(kind=kwargs.get("mask_generator_kind"), kwargs=kwargs.get("mask_gen_kwargs"))
|
||||
|
||||
if transform_variant is not None:
|
||||
transform = get_transforms(transform_variant, out_size)
|
||||
|
||||
if kind == 'default':
|
||||
dataset = InpaintingEvaluationDataset(indir, **kwargs)
|
||||
elif kind == 'our_eval':
|
||||
dataset = OurInpaintingEvaluationDataset(indir, **kwargs)
|
||||
elif kind == 'img_with_segm':
|
||||
dataset = ImgSegmentationDataset(indir=indir,
|
||||
mask_generator=mask_generator,
|
||||
transform=transform,
|
||||
out_size=out_size,
|
||||
**kwargs)
|
||||
elif kind == 'online':
|
||||
dataset = InpaintingEvalOnlineDataset(indir=indir,
|
||||
mask_generator=mask_generator,
|
||||
transform=transform,
|
||||
out_size=out_size,
|
||||
**kwargs)
|
||||
else:
|
||||
raise ValueError(f'Unknown val dataset kind {kind}')
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
def make_default_val_dataloader(*args, dataloader_kwargs=None, **kwargs):
|
||||
dataset = make_default_val_dataset(*args, **kwargs)
|
||||
|
||||
if dataloader_kwargs is None:
|
||||
dataloader_kwargs = {}
|
||||
dataloader = DataLoader(dataset, **dataloader_kwargs)
|
||||
return dataloader
|
||||
|
||||
|
||||
def make_constant_area_crop_params(img_height, img_width, min_size=128, max_size=512, area=256*256, round_to_mod=16):
|
||||
min_size = min(img_height, img_width, min_size)
|
||||
max_size = min(img_height, img_width, max_size)
|
||||
if random.random() < 0.5:
|
||||
out_height = min(max_size, ceil_modulo(random.randint(min_size, max_size), round_to_mod))
|
||||
out_width = min(max_size, ceil_modulo(area // out_height, round_to_mod))
|
||||
else:
|
||||
out_width = min(max_size, ceil_modulo(random.randint(min_size, max_size), round_to_mod))
|
||||
out_height = min(max_size, ceil_modulo(area // out_width, round_to_mod))
|
||||
|
||||
start_y = random.randint(0, img_height - out_height)
|
||||
start_x = random.randint(0, img_width - out_width)
|
||||
return (start_y, start_x, out_height, out_width)
|
||||
@@ -0,0 +1,332 @@
|
||||
import math
|
||||
import random
|
||||
import hashlib
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from saicinpainting.evaluation.masks.mask import SegmentationMask
|
||||
from saicinpainting.utils import LinearRamp
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DrawMethod(Enum):
|
||||
LINE = 'line'
|
||||
CIRCLE = 'circle'
|
||||
SQUARE = 'square'
|
||||
|
||||
|
||||
def make_random_irregular_mask(shape, max_angle=4, max_len=60, max_width=20, min_times=0, max_times=10,
|
||||
draw_method=DrawMethod.LINE):
|
||||
draw_method = DrawMethod(draw_method)
|
||||
|
||||
height, width = shape
|
||||
mask = np.zeros((height, width), np.float32)
|
||||
times = np.random.randint(min_times, max_times + 1)
|
||||
for i in range(times):
|
||||
start_x = np.random.randint(width)
|
||||
start_y = np.random.randint(height)
|
||||
for j in range(1 + np.random.randint(5)):
|
||||
angle = 0.01 + np.random.randint(max_angle)
|
||||
if i % 2 == 0:
|
||||
angle = 2 * 3.1415926 - angle
|
||||
length = 10 + np.random.randint(max_len)
|
||||
brush_w = 5 + np.random.randint(max_width)
|
||||
end_x = np.clip((start_x + length * np.sin(angle)).astype(np.int32), 0, width)
|
||||
end_y = np.clip((start_y + length * np.cos(angle)).astype(np.int32), 0, height)
|
||||
if draw_method == DrawMethod.LINE:
|
||||
cv2.line(mask, (start_x, start_y), (end_x, end_y), 1.0, brush_w)
|
||||
elif draw_method == DrawMethod.CIRCLE:
|
||||
cv2.circle(mask, (start_x, start_y), radius=brush_w, color=1., thickness=-1)
|
||||
elif draw_method == DrawMethod.SQUARE:
|
||||
radius = brush_w // 2
|
||||
mask[start_y - radius:start_y + radius, start_x - radius:start_x + radius] = 1
|
||||
start_x, start_y = end_x, end_y
|
||||
return mask[None, ...]
|
||||
|
||||
|
||||
class RandomIrregularMaskGenerator:
|
||||
def __init__(self, max_angle=4, max_len=60, max_width=20, min_times=0, max_times=10, ramp_kwargs=None,
|
||||
draw_method=DrawMethod.LINE):
|
||||
self.max_angle = max_angle
|
||||
self.max_len = max_len
|
||||
self.max_width = max_width
|
||||
self.min_times = min_times
|
||||
self.max_times = max_times
|
||||
self.draw_method = draw_method
|
||||
self.ramp = LinearRamp(**ramp_kwargs) if ramp_kwargs is not None else None
|
||||
|
||||
def __call__(self, img, iter_i=None, raw_image=None):
|
||||
coef = self.ramp(iter_i) if (self.ramp is not None) and (iter_i is not None) else 1
|
||||
cur_max_len = int(max(1, self.max_len * coef))
|
||||
cur_max_width = int(max(1, self.max_width * coef))
|
||||
cur_max_times = int(self.min_times + 1 + (self.max_times - self.min_times) * coef)
|
||||
return make_random_irregular_mask(img.shape[1:], max_angle=self.max_angle, max_len=cur_max_len,
|
||||
max_width=cur_max_width, min_times=self.min_times, max_times=cur_max_times,
|
||||
draw_method=self.draw_method)
|
||||
|
||||
|
||||
def make_random_rectangle_mask(shape, margin=10, bbox_min_size=30, bbox_max_size=100, min_times=0, max_times=3):
|
||||
height, width = shape
|
||||
mask = np.zeros((height, width), np.float32)
|
||||
bbox_max_size = min(bbox_max_size, height - margin * 2, width - margin * 2)
|
||||
times = np.random.randint(min_times, max_times + 1)
|
||||
for i in range(times):
|
||||
box_width = np.random.randint(bbox_min_size, bbox_max_size)
|
||||
box_height = np.random.randint(bbox_min_size, bbox_max_size)
|
||||
start_x = np.random.randint(margin, width - margin - box_width + 1)
|
||||
start_y = np.random.randint(margin, height - margin - box_height + 1)
|
||||
mask[start_y:start_y + box_height, start_x:start_x + box_width] = 1
|
||||
return mask[None, ...]
|
||||
|
||||
|
||||
class RandomRectangleMaskGenerator:
|
||||
def __init__(self, margin=10, bbox_min_size=30, bbox_max_size=100, min_times=0, max_times=3, ramp_kwargs=None):
|
||||
self.margin = margin
|
||||
self.bbox_min_size = bbox_min_size
|
||||
self.bbox_max_size = bbox_max_size
|
||||
self.min_times = min_times
|
||||
self.max_times = max_times
|
||||
self.ramp = LinearRamp(**ramp_kwargs) if ramp_kwargs is not None else None
|
||||
|
||||
def __call__(self, img, iter_i=None, raw_image=None):
|
||||
coef = self.ramp(iter_i) if (self.ramp is not None) and (iter_i is not None) else 1
|
||||
cur_bbox_max_size = int(self.bbox_min_size + 1 + (self.bbox_max_size - self.bbox_min_size) * coef)
|
||||
cur_max_times = int(self.min_times + (self.max_times - self.min_times) * coef)
|
||||
return make_random_rectangle_mask(img.shape[1:], margin=self.margin, bbox_min_size=self.bbox_min_size,
|
||||
bbox_max_size=cur_bbox_max_size, min_times=self.min_times,
|
||||
max_times=cur_max_times)
|
||||
|
||||
|
||||
class RandomSegmentationMaskGenerator:
|
||||
def __init__(self, **kwargs):
|
||||
self.impl = None # will be instantiated in first call (effectively in subprocess)
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(self, img, iter_i=None, raw_image=None):
|
||||
if self.impl is None:
|
||||
self.impl = SegmentationMask(**self.kwargs)
|
||||
|
||||
masks = self.impl.get_masks(np.transpose(img, (1, 2, 0)))
|
||||
masks = [m for m in masks if len(np.unique(m)) > 1]
|
||||
return np.random.choice(masks)
|
||||
|
||||
|
||||
def make_random_superres_mask(shape, min_step=2, max_step=4, min_width=1, max_width=3):
|
||||
height, width = shape
|
||||
mask = np.zeros((height, width), np.float32)
|
||||
step_x = np.random.randint(min_step, max_step + 1)
|
||||
width_x = np.random.randint(min_width, min(step_x, max_width + 1))
|
||||
offset_x = np.random.randint(0, step_x)
|
||||
|
||||
step_y = np.random.randint(min_step, max_step + 1)
|
||||
width_y = np.random.randint(min_width, min(step_y, max_width + 1))
|
||||
offset_y = np.random.randint(0, step_y)
|
||||
|
||||
for dy in range(width_y):
|
||||
mask[offset_y + dy::step_y] = 1
|
||||
for dx in range(width_x):
|
||||
mask[:, offset_x + dx::step_x] = 1
|
||||
return mask[None, ...]
|
||||
|
||||
|
||||
class RandomSuperresMaskGenerator:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(self, img, iter_i=None):
|
||||
return make_random_superres_mask(img.shape[1:], **self.kwargs)
|
||||
|
||||
|
||||
class DumbAreaMaskGenerator:
|
||||
min_ratio = 0.1
|
||||
max_ratio = 0.35
|
||||
default_ratio = 0.225
|
||||
|
||||
def __init__(self, is_training):
|
||||
#Parameters:
|
||||
# is_training(bool): If true - random rectangular mask, if false - central square mask
|
||||
self.is_training = is_training
|
||||
|
||||
def _random_vector(self, dimension):
|
||||
if self.is_training:
|
||||
lower_limit = math.sqrt(self.min_ratio)
|
||||
upper_limit = math.sqrt(self.max_ratio)
|
||||
mask_side = round((random.random() * (upper_limit - lower_limit) + lower_limit) * dimension)
|
||||
u = random.randint(0, dimension-mask_side-1)
|
||||
v = u+mask_side
|
||||
else:
|
||||
margin = (math.sqrt(self.default_ratio) / 2) * dimension
|
||||
u = round(dimension/2 - margin)
|
||||
v = round(dimension/2 + margin)
|
||||
return u, v
|
||||
|
||||
def __call__(self, img, iter_i=None, raw_image=None):
|
||||
c, height, width = img.shape
|
||||
mask = np.zeros((height, width), np.float32)
|
||||
x1, x2 = self._random_vector(width)
|
||||
y1, y2 = self._random_vector(height)
|
||||
mask[x1:x2, y1:y2] = 1
|
||||
return mask[None, ...]
|
||||
|
||||
|
||||
class OutpaintingMaskGenerator:
|
||||
def __init__(self, min_padding_percent:float=0.04, max_padding_percent:int=0.25, left_padding_prob:float=0.5, top_padding_prob:float=0.5,
|
||||
right_padding_prob:float=0.5, bottom_padding_prob:float=0.5, is_fixed_randomness:bool=False):
|
||||
"""
|
||||
is_fixed_randomness - get identical paddings for the same image if args are the same
|
||||
"""
|
||||
self.min_padding_percent = min_padding_percent
|
||||
self.max_padding_percent = max_padding_percent
|
||||
self.probs = [left_padding_prob, top_padding_prob, right_padding_prob, bottom_padding_prob]
|
||||
self.is_fixed_randomness = is_fixed_randomness
|
||||
|
||||
assert self.min_padding_percent <= self.max_padding_percent
|
||||
assert self.max_padding_percent > 0
|
||||
assert len([x for x in [self.min_padding_percent, self.max_padding_percent] if (x>=0 and x<=1)]) == 2, f"Padding percentage should be in [0,1]"
|
||||
assert sum(self.probs) > 0, f"At least one of the padding probs should be greater than 0 - {self.probs}"
|
||||
assert len([x for x in self.probs if (x >= 0) and (x <= 1)]) == 4, f"At least one of padding probs is not in [0,1] - {self.probs}"
|
||||
if len([x for x in self.probs if x > 0]) == 1:
|
||||
LOGGER.warning(f"Only one padding prob is greater than zero - {self.probs}. That means that the outpainting masks will be always on the same side")
|
||||
|
||||
def apply_padding(self, mask, coord):
|
||||
mask[int(coord[0][0]*self.img_h):int(coord[1][0]*self.img_h),
|
||||
int(coord[0][1]*self.img_w):int(coord[1][1]*self.img_w)] = 1
|
||||
return mask
|
||||
|
||||
def get_padding(self, size):
|
||||
n1 = int(self.min_padding_percent*size)
|
||||
n2 = int(self.max_padding_percent*size)
|
||||
return self.rnd.randint(n1, n2) / size
|
||||
|
||||
@staticmethod
|
||||
def _img2rs(img):
|
||||
arr = np.ascontiguousarray(img.astype(np.uint8))
|
||||
str_hash = hashlib.sha1(arr).hexdigest()
|
||||
res = hash(str_hash)%(2**32)
|
||||
return res
|
||||
|
||||
def __call__(self, img, iter_i=None, raw_image=None):
|
||||
c, self.img_h, self.img_w = img.shape
|
||||
mask = np.zeros((self.img_h, self.img_w), np.float32)
|
||||
at_least_one_mask_applied = False
|
||||
|
||||
if self.is_fixed_randomness:
|
||||
assert raw_image is not None, f"Cant calculate hash on raw_image=None"
|
||||
rs = self._img2rs(raw_image)
|
||||
self.rnd = np.random.RandomState(rs)
|
||||
else:
|
||||
self.rnd = np.random
|
||||
|
||||
coords = [[
|
||||
(0,0),
|
||||
(1,self.get_padding(size=self.img_h))
|
||||
],
|
||||
[
|
||||
(0,0),
|
||||
(self.get_padding(size=self.img_w),1)
|
||||
],
|
||||
[
|
||||
(0,1-self.get_padding(size=self.img_h)),
|
||||
(1,1)
|
||||
],
|
||||
[
|
||||
(1-self.get_padding(size=self.img_w),0),
|
||||
(1,1)
|
||||
]]
|
||||
|
||||
for pp, coord in zip(self.probs, coords):
|
||||
if self.rnd.random() < pp:
|
||||
at_least_one_mask_applied = True
|
||||
mask = self.apply_padding(mask=mask, coord=coord)
|
||||
|
||||
if not at_least_one_mask_applied:
|
||||
idx = self.rnd.choice(range(len(coords)), p=np.array(self.probs)/sum(self.probs))
|
||||
mask = self.apply_padding(mask=mask, coord=coords[idx])
|
||||
return mask[None, ...]
|
||||
|
||||
|
||||
class MixedMaskGenerator:
|
||||
def __init__(self, irregular_proba=1/3, irregular_kwargs=None,
|
||||
box_proba=1/3, box_kwargs=None,
|
||||
segm_proba=1/3, segm_kwargs=None,
|
||||
squares_proba=0, squares_kwargs=None,
|
||||
superres_proba=0, superres_kwargs=None,
|
||||
outpainting_proba=0, outpainting_kwargs=None,
|
||||
invert_proba=0):
|
||||
self.probas = []
|
||||
self.gens = []
|
||||
|
||||
if irregular_proba > 0:
|
||||
self.probas.append(irregular_proba)
|
||||
if irregular_kwargs is None:
|
||||
irregular_kwargs = {}
|
||||
else:
|
||||
irregular_kwargs = dict(irregular_kwargs)
|
||||
irregular_kwargs['draw_method'] = DrawMethod.LINE
|
||||
self.gens.append(RandomIrregularMaskGenerator(**irregular_kwargs))
|
||||
|
||||
if box_proba > 0:
|
||||
self.probas.append(box_proba)
|
||||
if box_kwargs is None:
|
||||
box_kwargs = {}
|
||||
self.gens.append(RandomRectangleMaskGenerator(**box_kwargs))
|
||||
|
||||
if segm_proba > 0:
|
||||
self.probas.append(segm_proba)
|
||||
if segm_kwargs is None:
|
||||
segm_kwargs = {}
|
||||
self.gens.append(RandomSegmentationMaskGenerator(**segm_kwargs))
|
||||
|
||||
if squares_proba > 0:
|
||||
self.probas.append(squares_proba)
|
||||
if squares_kwargs is None:
|
||||
squares_kwargs = {}
|
||||
else:
|
||||
squares_kwargs = dict(squares_kwargs)
|
||||
squares_kwargs['draw_method'] = DrawMethod.SQUARE
|
||||
self.gens.append(RandomIrregularMaskGenerator(**squares_kwargs))
|
||||
|
||||
if superres_proba > 0:
|
||||
self.probas.append(superres_proba)
|
||||
if superres_kwargs is None:
|
||||
superres_kwargs = {}
|
||||
self.gens.append(RandomSuperresMaskGenerator(**superres_kwargs))
|
||||
|
||||
if outpainting_proba > 0:
|
||||
self.probas.append(outpainting_proba)
|
||||
if outpainting_kwargs is None:
|
||||
outpainting_kwargs = {}
|
||||
self.gens.append(OutpaintingMaskGenerator(**outpainting_kwargs))
|
||||
|
||||
self.probas = np.array(self.probas, dtype='float32')
|
||||
self.probas /= self.probas.sum()
|
||||
self.invert_proba = invert_proba
|
||||
|
||||
def __call__(self, img, iter_i=None, raw_image=None):
|
||||
kind = np.random.choice(len(self.probas), p=self.probas)
|
||||
gen = self.gens[kind]
|
||||
result = gen(img, iter_i=iter_i, raw_image=raw_image)
|
||||
if self.invert_proba > 0 and random.random() < self.invert_proba:
|
||||
result = 1 - result
|
||||
return result
|
||||
|
||||
|
||||
def get_mask_generator(kind, kwargs):
|
||||
if kind is None:
|
||||
kind = "mixed"
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
if kind == "mixed":
|
||||
cl = MixedMaskGenerator
|
||||
elif kind == "outpainting":
|
||||
cl = OutpaintingMaskGenerator
|
||||
elif kind == "dumb":
|
||||
cl = DumbAreaMaskGenerator
|
||||
else:
|
||||
raise NotImplementedError(f"No such generator kind = {kind}")
|
||||
return cl(**kwargs)
|
||||
@@ -0,0 +1,177 @@
|
||||
from typing import Tuple, Dict, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class BaseAdversarialLoss:
|
||||
def pre_generator_step(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
generator: nn.Module, discriminator: nn.Module):
|
||||
"""
|
||||
Prepare for generator step
|
||||
:param real_batch: Tensor, a batch of real samples
|
||||
:param fake_batch: Tensor, a batch of samples produced by generator
|
||||
:param generator:
|
||||
:param discriminator:
|
||||
:return: None
|
||||
"""
|
||||
|
||||
def pre_discriminator_step(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
generator: nn.Module, discriminator: nn.Module):
|
||||
"""
|
||||
Prepare for discriminator step
|
||||
:param real_batch: Tensor, a batch of real samples
|
||||
:param fake_batch: Tensor, a batch of samples produced by generator
|
||||
:param generator:
|
||||
:param discriminator:
|
||||
:return: None
|
||||
"""
|
||||
|
||||
def generator_loss(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
discr_real_pred: torch.Tensor, discr_fake_pred: torch.Tensor,
|
||||
mask: Optional[torch.Tensor] = None) \
|
||||
-> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Calculate generator loss
|
||||
:param real_batch: Tensor, a batch of real samples
|
||||
:param fake_batch: Tensor, a batch of samples produced by generator
|
||||
:param discr_real_pred: Tensor, discriminator output for real_batch
|
||||
:param discr_fake_pred: Tensor, discriminator output for fake_batch
|
||||
:param mask: Tensor, actual mask, which was at input of generator when making fake_batch
|
||||
:return: total generator loss along with some values that might be interesting to log
|
||||
"""
|
||||
raise NotImplemented()
|
||||
|
||||
def discriminator_loss(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
discr_real_pred: torch.Tensor, discr_fake_pred: torch.Tensor,
|
||||
mask: Optional[torch.Tensor] = None) \
|
||||
-> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
"""
|
||||
Calculate discriminator loss and call .backward() on it
|
||||
:param real_batch: Tensor, a batch of real samples
|
||||
:param fake_batch: Tensor, a batch of samples produced by generator
|
||||
:param discr_real_pred: Tensor, discriminator output for real_batch
|
||||
:param discr_fake_pred: Tensor, discriminator output for fake_batch
|
||||
:param mask: Tensor, actual mask, which was at input of generator when making fake_batch
|
||||
:return: total discriminator loss along with some values that might be interesting to log
|
||||
"""
|
||||
raise NotImplemented()
|
||||
|
||||
def interpolate_mask(self, mask, shape):
|
||||
assert mask is not None
|
||||
assert self.allow_scale_mask or shape == mask.shape[-2:]
|
||||
if shape != mask.shape[-2:] and self.allow_scale_mask:
|
||||
if self.mask_scale_mode == 'maxpool':
|
||||
mask = F.adaptive_max_pool2d(mask, shape)
|
||||
else:
|
||||
mask = F.interpolate(mask, size=shape, mode=self.mask_scale_mode)
|
||||
return mask
|
||||
|
||||
def make_r1_gp(discr_real_pred, real_batch):
|
||||
if torch.is_grad_enabled():
|
||||
grad_real = torch.autograd.grad(outputs=discr_real_pred.sum(), inputs=real_batch, create_graph=True)[0]
|
||||
grad_penalty = (grad_real.view(grad_real.shape[0], -1).norm(2, dim=1) ** 2).mean()
|
||||
else:
|
||||
grad_penalty = 0
|
||||
real_batch.requires_grad = False
|
||||
|
||||
return grad_penalty
|
||||
|
||||
class NonSaturatingWithR1(BaseAdversarialLoss):
|
||||
def __init__(self, gp_coef=5, weight=1, mask_as_fake_target=False, allow_scale_mask=False,
|
||||
mask_scale_mode='nearest', extra_mask_weight_for_gen=0,
|
||||
use_unmasked_for_gen=True, use_unmasked_for_discr=True):
|
||||
self.gp_coef = gp_coef
|
||||
self.weight = weight
|
||||
# use for discr => use for gen;
|
||||
# otherwise we teach only the discr to pay attention to very small difference
|
||||
assert use_unmasked_for_gen or (not use_unmasked_for_discr)
|
||||
# mask as target => use unmasked for discr:
|
||||
# if we don't care about unmasked regions at all
|
||||
# then it doesn't matter if the value of mask_as_fake_target is true or false
|
||||
assert use_unmasked_for_discr or (not mask_as_fake_target)
|
||||
self.use_unmasked_for_gen = use_unmasked_for_gen
|
||||
self.use_unmasked_for_discr = use_unmasked_for_discr
|
||||
self.mask_as_fake_target = mask_as_fake_target
|
||||
self.allow_scale_mask = allow_scale_mask
|
||||
self.mask_scale_mode = mask_scale_mode
|
||||
self.extra_mask_weight_for_gen = extra_mask_weight_for_gen
|
||||
|
||||
def generator_loss(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
discr_real_pred: torch.Tensor, discr_fake_pred: torch.Tensor,
|
||||
mask=None) \
|
||||
-> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
fake_loss = F.softplus(-discr_fake_pred)
|
||||
if (self.mask_as_fake_target and self.extra_mask_weight_for_gen > 0) or \
|
||||
not self.use_unmasked_for_gen: # == if masked region should be treated differently
|
||||
mask = self.interpolate_mask(mask, discr_fake_pred.shape[-2:])
|
||||
if not self.use_unmasked_for_gen:
|
||||
fake_loss = fake_loss * mask
|
||||
else:
|
||||
pixel_weights = 1 + mask * self.extra_mask_weight_for_gen
|
||||
fake_loss = fake_loss * pixel_weights
|
||||
|
||||
return fake_loss.mean() * self.weight, dict()
|
||||
|
||||
def pre_discriminator_step(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
generator: nn.Module, discriminator: nn.Module):
|
||||
real_batch.requires_grad = True
|
||||
|
||||
def discriminator_loss(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
discr_real_pred: torch.Tensor, discr_fake_pred: torch.Tensor,
|
||||
mask=None) \
|
||||
-> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
|
||||
real_loss = F.softplus(-discr_real_pred)
|
||||
grad_penalty = make_r1_gp(discr_real_pred, real_batch) * self.gp_coef
|
||||
fake_loss = F.softplus(discr_fake_pred)
|
||||
|
||||
if not self.use_unmasked_for_discr or self.mask_as_fake_target:
|
||||
# == if masked region should be treated differently
|
||||
mask = self.interpolate_mask(mask, discr_fake_pred.shape[-2:])
|
||||
# use_unmasked_for_discr=False only makes sense for fakes;
|
||||
# for reals there is no difference beetween two regions
|
||||
fake_loss = fake_loss * mask
|
||||
if self.mask_as_fake_target:
|
||||
fake_loss = fake_loss + (1 - mask) * F.softplus(-discr_fake_pred)
|
||||
|
||||
sum_discr_loss = real_loss + grad_penalty + fake_loss
|
||||
metrics = dict(discr_real_out=discr_real_pred.mean(),
|
||||
discr_fake_out=discr_fake_pred.mean(),
|
||||
discr_real_gp=grad_penalty)
|
||||
return sum_discr_loss.mean(), metrics
|
||||
|
||||
class BCELoss(BaseAdversarialLoss):
|
||||
def __init__(self, weight):
|
||||
self.weight = weight
|
||||
self.bce_loss = nn.BCEWithLogitsLoss()
|
||||
|
||||
def generator_loss(self, discr_fake_pred: torch.Tensor) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
real_mask_gt = torch.zeros(discr_fake_pred.shape).to(discr_fake_pred.device)
|
||||
fake_loss = self.bce_loss(discr_fake_pred, real_mask_gt) * self.weight
|
||||
return fake_loss, dict()
|
||||
|
||||
def pre_discriminator_step(self, real_batch: torch.Tensor, fake_batch: torch.Tensor,
|
||||
generator: nn.Module, discriminator: nn.Module):
|
||||
real_batch.requires_grad = True
|
||||
|
||||
def discriminator_loss(self,
|
||||
mask: torch.Tensor,
|
||||
discr_real_pred: torch.Tensor,
|
||||
discr_fake_pred: torch.Tensor) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
|
||||
real_mask_gt = torch.zeros(discr_real_pred.shape).to(discr_real_pred.device)
|
||||
sum_discr_loss = (self.bce_loss(discr_real_pred, real_mask_gt) + self.bce_loss(discr_fake_pred, mask)) / 2
|
||||
metrics = dict(discr_real_out=discr_real_pred.mean(),
|
||||
discr_fake_out=discr_fake_pred.mean(),
|
||||
discr_real_gp=0)
|
||||
return sum_discr_loss, metrics
|
||||
|
||||
|
||||
def make_discrim_loss(kind, **kwargs):
|
||||
if kind == 'r1':
|
||||
return NonSaturatingWithR1(**kwargs)
|
||||
elif kind == 'bce':
|
||||
return BCELoss(**kwargs)
|
||||
raise ValueError(f'Unknown adversarial loss kind {kind}')
|
||||
@@ -0,0 +1,152 @@
|
||||
weights = {"ade20k":
|
||||
[6.34517766497462,
|
||||
9.328358208955224,
|
||||
11.389521640091116,
|
||||
16.10305958132045,
|
||||
20.833333333333332,
|
||||
22.22222222222222,
|
||||
25.125628140703515,
|
||||
43.29004329004329,
|
||||
50.5050505050505,
|
||||
54.6448087431694,
|
||||
55.24861878453038,
|
||||
60.24096385542168,
|
||||
62.5,
|
||||
66.2251655629139,
|
||||
84.74576271186442,
|
||||
90.90909090909092,
|
||||
91.74311926605505,
|
||||
96.15384615384616,
|
||||
96.15384615384616,
|
||||
97.08737864077669,
|
||||
102.04081632653062,
|
||||
135.13513513513513,
|
||||
149.2537313432836,
|
||||
153.84615384615384,
|
||||
163.93442622950818,
|
||||
166.66666666666666,
|
||||
188.67924528301887,
|
||||
192.30769230769232,
|
||||
217.3913043478261,
|
||||
227.27272727272725,
|
||||
227.27272727272725,
|
||||
227.27272727272725,
|
||||
303.03030303030306,
|
||||
322.5806451612903,
|
||||
333.3333333333333,
|
||||
370.3703703703703,
|
||||
384.61538461538464,
|
||||
416.6666666666667,
|
||||
416.6666666666667,
|
||||
434.7826086956522,
|
||||
434.7826086956522,
|
||||
454.5454545454545,
|
||||
454.5454545454545,
|
||||
500.0,
|
||||
526.3157894736842,
|
||||
526.3157894736842,
|
||||
555.5555555555555,
|
||||
555.5555555555555,
|
||||
555.5555555555555,
|
||||
555.5555555555555,
|
||||
555.5555555555555,
|
||||
555.5555555555555,
|
||||
555.5555555555555,
|
||||
588.2352941176471,
|
||||
588.2352941176471,
|
||||
588.2352941176471,
|
||||
588.2352941176471,
|
||||
588.2352941176471,
|
||||
666.6666666666666,
|
||||
666.6666666666666,
|
||||
666.6666666666666,
|
||||
666.6666666666666,
|
||||
714.2857142857143,
|
||||
714.2857142857143,
|
||||
714.2857142857143,
|
||||
714.2857142857143,
|
||||
714.2857142857143,
|
||||
769.2307692307693,
|
||||
769.2307692307693,
|
||||
769.2307692307693,
|
||||
833.3333333333334,
|
||||
833.3333333333334,
|
||||
833.3333333333334,
|
||||
833.3333333333334,
|
||||
909.090909090909,
|
||||
1000.0,
|
||||
1111.111111111111,
|
||||
1111.111111111111,
|
||||
1111.111111111111,
|
||||
1111.111111111111,
|
||||
1111.111111111111,
|
||||
1250.0,
|
||||
1250.0,
|
||||
1250.0,
|
||||
1250.0,
|
||||
1250.0,
|
||||
1428.5714285714287,
|
||||
1428.5714285714287,
|
||||
1428.5714285714287,
|
||||
1428.5714285714287,
|
||||
1428.5714285714287,
|
||||
1428.5714285714287,
|
||||
1428.5714285714287,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
1666.6666666666667,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2000.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
2500.0,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
3333.3333333333335,
|
||||
5000.0,
|
||||
5000.0,
|
||||
5000.0]
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchvision
|
||||
|
||||
from saicinpainting.training.losses.perceptual import IMAGENET_STD, IMAGENET_MEAN
|
||||
|
||||
|
||||
def dummy_distance_weighter(real_img, pred_img, mask):
|
||||
return mask
|
||||
|
||||
|
||||
def get_gauss_kernel(kernel_size, width_factor=1):
|
||||
coords = torch.stack(torch.meshgrid(torch.arange(kernel_size),
|
||||
torch.arange(kernel_size)),
|
||||
dim=0).float()
|
||||
diff = torch.exp(-((coords - kernel_size // 2) ** 2).sum(0) / kernel_size / width_factor)
|
||||
diff /= diff.sum()
|
||||
return diff
|
||||
|
||||
|
||||
class BlurMask(nn.Module):
|
||||
def __init__(self, kernel_size=5, width_factor=1):
|
||||
super().__init__()
|
||||
self.filter = nn.Conv2d(1, 1, kernel_size, padding=kernel_size // 2, padding_mode='replicate', bias=False)
|
||||
self.filter.weight.data.copy_(get_gauss_kernel(kernel_size, width_factor=width_factor))
|
||||
|
||||
def forward(self, real_img, pred_img, mask):
|
||||
with torch.no_grad():
|
||||
result = self.filter(mask) * mask
|
||||
return result
|
||||
|
||||
|
||||
class EmulatedEDTMask(nn.Module):
|
||||
def __init__(self, dilate_kernel_size=5, blur_kernel_size=5, width_factor=1):
|
||||
super().__init__()
|
||||
self.dilate_filter = nn.Conv2d(1, 1, dilate_kernel_size, padding=dilate_kernel_size// 2, padding_mode='replicate',
|
||||
bias=False)
|
||||
self.dilate_filter.weight.data.copy_(torch.ones(1, 1, dilate_kernel_size, dilate_kernel_size, dtype=torch.float))
|
||||
self.blur_filter = nn.Conv2d(1, 1, blur_kernel_size, padding=blur_kernel_size // 2, padding_mode='replicate', bias=False)
|
||||
self.blur_filter.weight.data.copy_(get_gauss_kernel(blur_kernel_size, width_factor=width_factor))
|
||||
|
||||
def forward(self, real_img, pred_img, mask):
|
||||
with torch.no_grad():
|
||||
known_mask = 1 - mask
|
||||
dilated_known_mask = (self.dilate_filter(known_mask) > 1).float()
|
||||
result = self.blur_filter(1 - dilated_known_mask) * mask
|
||||
return result
|
||||
|
||||
|
||||
class PropagatePerceptualSim(nn.Module):
|
||||
def __init__(self, level=2, max_iters=10, temperature=500, erode_mask_size=3):
|
||||
super().__init__()
|
||||
vgg = torchvision.models.vgg19(pretrained=True).features
|
||||
vgg_avg_pooling = []
|
||||
|
||||
for weights in vgg.parameters():
|
||||
weights.requires_grad = False
|
||||
|
||||
cur_level_i = 0
|
||||
for module in vgg.modules():
|
||||
if module.__class__.__name__ == 'Sequential':
|
||||
continue
|
||||
elif module.__class__.__name__ == 'MaxPool2d':
|
||||
vgg_avg_pooling.append(nn.AvgPool2d(kernel_size=2, stride=2, padding=0))
|
||||
else:
|
||||
vgg_avg_pooling.append(module)
|
||||
if module.__class__.__name__ == 'ReLU':
|
||||
cur_level_i += 1
|
||||
if cur_level_i == level:
|
||||
break
|
||||
|
||||
self.features = nn.Sequential(*vgg_avg_pooling)
|
||||
|
||||
self.max_iters = max_iters
|
||||
self.temperature = temperature
|
||||
self.do_erode = erode_mask_size > 0
|
||||
if self.do_erode:
|
||||
self.erode_mask = nn.Conv2d(1, 1, erode_mask_size, padding=erode_mask_size // 2, bias=False)
|
||||
self.erode_mask.weight.data.fill_(1)
|
||||
|
||||
def forward(self, real_img, pred_img, mask):
|
||||
with torch.no_grad():
|
||||
real_img = (real_img - IMAGENET_MEAN.to(real_img)) / IMAGENET_STD.to(real_img)
|
||||
real_feats = self.features(real_img)
|
||||
|
||||
vertical_sim = torch.exp(-(real_feats[:, :, 1:] - real_feats[:, :, :-1]).pow(2).sum(1, keepdim=True)
|
||||
/ self.temperature)
|
||||
horizontal_sim = torch.exp(-(real_feats[:, :, :, 1:] - real_feats[:, :, :, :-1]).pow(2).sum(1, keepdim=True)
|
||||
/ self.temperature)
|
||||
|
||||
mask_scaled = F.interpolate(mask, size=real_feats.shape[-2:], mode='bilinear', align_corners=False)
|
||||
if self.do_erode:
|
||||
mask_scaled = (self.erode_mask(mask_scaled) > 1).float()
|
||||
|
||||
cur_knowness = 1 - mask_scaled
|
||||
|
||||
for iter_i in range(self.max_iters):
|
||||
new_top_knowness = F.pad(cur_knowness[:, :, :-1] * vertical_sim, (0, 0, 1, 0), mode='replicate')
|
||||
new_bottom_knowness = F.pad(cur_knowness[:, :, 1:] * vertical_sim, (0, 0, 0, 1), mode='replicate')
|
||||
|
||||
new_left_knowness = F.pad(cur_knowness[:, :, :, :-1] * horizontal_sim, (1, 0, 0, 0), mode='replicate')
|
||||
new_right_knowness = F.pad(cur_knowness[:, :, :, 1:] * horizontal_sim, (0, 1, 0, 0), mode='replicate')
|
||||
|
||||
new_knowness = torch.stack([new_top_knowness, new_bottom_knowness,
|
||||
new_left_knowness, new_right_knowness],
|
||||
dim=0).max(0).values
|
||||
|
||||
cur_knowness = torch.max(cur_knowness, new_knowness)
|
||||
|
||||
cur_knowness = F.interpolate(cur_knowness, size=mask.shape[-2:], mode='bilinear')
|
||||
result = torch.min(mask, 1 - cur_knowness)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def make_mask_distance_weighter(kind='none', **kwargs):
|
||||
if kind == 'none':
|
||||
return dummy_distance_weighter
|
||||
if kind == 'blur':
|
||||
return BlurMask(**kwargs)
|
||||
if kind == 'edt':
|
||||
return EmulatedEDTMask(**kwargs)
|
||||
if kind == 'pps':
|
||||
return PropagatePerceptualSim(**kwargs)
|
||||
raise ValueError(f'Unknown mask distance weighter kind {kind}')
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def masked_l2_loss(pred, target, mask, weight_known, weight_missing):
|
||||
per_pixel_l2 = F.mse_loss(pred, target, reduction='none')
|
||||
pixel_weights = mask * weight_missing + (1 - mask) * weight_known
|
||||
return (pixel_weights * per_pixel_l2).mean()
|
||||
|
||||
|
||||
def masked_l1_loss(pred, target, mask, weight_known, weight_missing):
|
||||
per_pixel_l1 = F.l1_loss(pred, target, reduction='none')
|
||||
pixel_weights = mask * weight_missing + (1 - mask) * weight_known
|
||||
return (pixel_weights * per_pixel_l1).mean()
|
||||
|
||||
|
||||
def feature_matching_loss(fake_features: List[torch.Tensor], target_features: List[torch.Tensor], mask=None):
|
||||
if mask is None:
|
||||
res = torch.stack([F.mse_loss(fake_feat, target_feat)
|
||||
for fake_feat, target_feat in zip(fake_features, target_features)]).mean()
|
||||
else:
|
||||
res = 0
|
||||
norm = 0
|
||||
for fake_feat, target_feat in zip(fake_features, target_features):
|
||||
cur_mask = F.interpolate(mask, size=fake_feat.shape[-2:], mode='bilinear', align_corners=False)
|
||||
error_weights = 1 - cur_mask
|
||||
cur_val = ((fake_feat - target_feat).pow(2) * error_weights).mean()
|
||||
res = res + cur_val
|
||||
norm += 1
|
||||
res = res / norm
|
||||
return res
|
||||
@@ -0,0 +1,113 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torchvision
|
||||
|
||||
from models.ade20k import ModelBuilder
|
||||
from saicinpainting.utils import check_and_warn_input_range
|
||||
|
||||
|
||||
IMAGENET_MEAN = torch.FloatTensor([0.485, 0.456, 0.406])[None, :, None, None]
|
||||
IMAGENET_STD = torch.FloatTensor([0.229, 0.224, 0.225])[None, :, None, None]
|
||||
|
||||
|
||||
class PerceptualLoss(nn.Module):
|
||||
def __init__(self, normalize_inputs=True):
|
||||
super(PerceptualLoss, self).__init__()
|
||||
|
||||
self.normalize_inputs = normalize_inputs
|
||||
self.mean_ = IMAGENET_MEAN
|
||||
self.std_ = IMAGENET_STD
|
||||
|
||||
vgg = torchvision.models.vgg19(pretrained=True).features
|
||||
vgg_avg_pooling = []
|
||||
|
||||
for weights in vgg.parameters():
|
||||
weights.requires_grad = False
|
||||
|
||||
for module in vgg.modules():
|
||||
if module.__class__.__name__ == 'Sequential':
|
||||
continue
|
||||
elif module.__class__.__name__ == 'MaxPool2d':
|
||||
vgg_avg_pooling.append(nn.AvgPool2d(kernel_size=2, stride=2, padding=0))
|
||||
else:
|
||||
vgg_avg_pooling.append(module)
|
||||
|
||||
self.vgg = nn.Sequential(*vgg_avg_pooling)
|
||||
|
||||
def do_normalize_inputs(self, x):
|
||||
return (x - self.mean_.to(x.device)) / self.std_.to(x.device)
|
||||
|
||||
def partial_losses(self, input, target, mask=None):
|
||||
check_and_warn_input_range(target, 0, 1, 'PerceptualLoss target in partial_losses')
|
||||
|
||||
# we expect input and target to be in [0, 1] range
|
||||
losses = []
|
||||
|
||||
if self.normalize_inputs:
|
||||
features_input = self.do_normalize_inputs(input)
|
||||
features_target = self.do_normalize_inputs(target)
|
||||
else:
|
||||
features_input = input
|
||||
features_target = target
|
||||
|
||||
for layer in self.vgg[:30]:
|
||||
|
||||
features_input = layer(features_input)
|
||||
features_target = layer(features_target)
|
||||
|
||||
if layer.__class__.__name__ == 'ReLU':
|
||||
loss = F.mse_loss(features_input, features_target, reduction='none')
|
||||
|
||||
if mask is not None:
|
||||
cur_mask = F.interpolate(mask, size=features_input.shape[-2:],
|
||||
mode='bilinear', align_corners=False)
|
||||
loss = loss * (1 - cur_mask)
|
||||
|
||||
loss = loss.mean(dim=tuple(range(1, len(loss.shape))))
|
||||
losses.append(loss)
|
||||
|
||||
return losses
|
||||
|
||||
def forward(self, input, target, mask=None):
|
||||
losses = self.partial_losses(input, target, mask=mask)
|
||||
return torch.stack(losses).sum(dim=0)
|
||||
|
||||
def get_global_features(self, input):
|
||||
check_and_warn_input_range(input, 0, 1, 'PerceptualLoss input in get_global_features')
|
||||
|
||||
if self.normalize_inputs:
|
||||
features_input = self.do_normalize_inputs(input)
|
||||
else:
|
||||
features_input = input
|
||||
|
||||
features_input = self.vgg(features_input)
|
||||
return features_input
|
||||
|
||||
|
||||
class ResNetPL(nn.Module):
|
||||
def __init__(self, weight=1,
|
||||
weights_path=None, arch_encoder='resnet50dilated', segmentation=True):
|
||||
super().__init__()
|
||||
self.impl = ModelBuilder.get_encoder(weights_path=weights_path,
|
||||
arch_encoder=arch_encoder,
|
||||
arch_decoder='ppm_deepsup',
|
||||
fc_dim=2048,
|
||||
segmentation=segmentation)
|
||||
self.impl.eval()
|
||||
for w in self.impl.parameters():
|
||||
w.requires_grad_(False)
|
||||
|
||||
self.weight = weight
|
||||
|
||||
def forward(self, pred, target):
|
||||
pred = (pred - IMAGENET_MEAN.to(pred)) / IMAGENET_STD.to(pred)
|
||||
target = (target - IMAGENET_MEAN.to(target)) / IMAGENET_STD.to(target)
|
||||
|
||||
pred_feats = self.impl(pred, return_feature_maps=True)
|
||||
target_feats = self.impl(target, return_feature_maps=True)
|
||||
|
||||
result = torch.stack([F.mse_loss(cur_pred, cur_target)
|
||||
for cur_pred, cur_target
|
||||
in zip(pred_feats, target_feats)]).sum() * self.weight
|
||||
return result
|
||||
@@ -0,0 +1,43 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .constants import weights as constant_weights
|
||||
|
||||
|
||||
class CrossEntropy2d(nn.Module):
|
||||
def __init__(self, reduction="mean", ignore_label=255, weights=None, *args, **kwargs):
|
||||
"""
|
||||
weight (Tensor, optional): a manual rescaling weight given to each class.
|
||||
If given, has to be a Tensor of size "nclasses"
|
||||
"""
|
||||
super(CrossEntropy2d, self).__init__()
|
||||
self.reduction = reduction
|
||||
self.ignore_label = ignore_label
|
||||
self.weights = weights
|
||||
if self.weights is not None:
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
self.weights = torch.FloatTensor(constant_weights[weights]).to(device)
|
||||
|
||||
def forward(self, predict, target):
|
||||
"""
|
||||
Args:
|
||||
predict:(n, c, h, w)
|
||||
target:(n, 1, h, w)
|
||||
"""
|
||||
target = target.long()
|
||||
assert not target.requires_grad
|
||||
assert predict.dim() == 4, "{0}".format(predict.size())
|
||||
assert target.dim() == 4, "{0}".format(target.size())
|
||||
assert predict.size(0) == target.size(0), "{0} vs {1} ".format(predict.size(0), target.size(0))
|
||||
assert target.size(1) == 1, "{0}".format(target.size(1))
|
||||
assert predict.size(2) == target.size(2), "{0} vs {1} ".format(predict.size(2), target.size(2))
|
||||
assert predict.size(3) == target.size(3), "{0} vs {1} ".format(predict.size(3), target.size(3))
|
||||
target = target.squeeze(1)
|
||||
n, c, h, w = predict.size()
|
||||
target_mask = (target >= 0) * (target != self.ignore_label)
|
||||
target = target[target_mask]
|
||||
predict = predict.transpose(1, 2).transpose(2, 3).contiguous()
|
||||
predict = predict[target_mask.view(n, h, w, 1).repeat(1, 1, 1, c)].view(-1, c)
|
||||
loss = F.cross_entropy(predict, target, weight=self.weights, reduction=self.reduction)
|
||||
return loss
|
||||
@@ -0,0 +1,155 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torchvision.models as models
|
||||
|
||||
|
||||
class PerceptualLoss(nn.Module):
|
||||
r"""
|
||||
Perceptual loss, VGG-based
|
||||
https://arxiv.org/abs/1603.08155
|
||||
https://github.com/dxyang/StyleTransfer/blob/master/utils.py
|
||||
"""
|
||||
|
||||
def __init__(self, weights=[1.0, 1.0, 1.0, 1.0, 1.0]):
|
||||
super(PerceptualLoss, self).__init__()
|
||||
self.add_module('vgg', VGG19())
|
||||
self.criterion = torch.nn.L1Loss()
|
||||
self.weights = weights
|
||||
|
||||
def __call__(self, x, y):
|
||||
# Compute features
|
||||
x_vgg, y_vgg = self.vgg(x), self.vgg(y)
|
||||
|
||||
content_loss = 0.0
|
||||
content_loss += self.weights[0] * self.criterion(x_vgg['relu1_1'], y_vgg['relu1_1'])
|
||||
content_loss += self.weights[1] * self.criterion(x_vgg['relu2_1'], y_vgg['relu2_1'])
|
||||
content_loss += self.weights[2] * self.criterion(x_vgg['relu3_1'], y_vgg['relu3_1'])
|
||||
content_loss += self.weights[3] * self.criterion(x_vgg['relu4_1'], y_vgg['relu4_1'])
|
||||
content_loss += self.weights[4] * self.criterion(x_vgg['relu5_1'], y_vgg['relu5_1'])
|
||||
|
||||
|
||||
return content_loss
|
||||
|
||||
|
||||
class VGG19(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super(VGG19, self).__init__()
|
||||
features = models.vgg19(pretrained=True).features
|
||||
self.relu1_1 = torch.nn.Sequential()
|
||||
self.relu1_2 = torch.nn.Sequential()
|
||||
|
||||
self.relu2_1 = torch.nn.Sequential()
|
||||
self.relu2_2 = torch.nn.Sequential()
|
||||
|
||||
self.relu3_1 = torch.nn.Sequential()
|
||||
self.relu3_2 = torch.nn.Sequential()
|
||||
self.relu3_3 = torch.nn.Sequential()
|
||||
self.relu3_4 = torch.nn.Sequential()
|
||||
|
||||
self.relu4_1 = torch.nn.Sequential()
|
||||
self.relu4_2 = torch.nn.Sequential()
|
||||
self.relu4_3 = torch.nn.Sequential()
|
||||
self.relu4_4 = torch.nn.Sequential()
|
||||
|
||||
self.relu5_1 = torch.nn.Sequential()
|
||||
self.relu5_2 = torch.nn.Sequential()
|
||||
self.relu5_3 = torch.nn.Sequential()
|
||||
self.relu5_4 = torch.nn.Sequential()
|
||||
|
||||
for x in range(2):
|
||||
self.relu1_1.add_module(str(x), features[x])
|
||||
|
||||
for x in range(2, 4):
|
||||
self.relu1_2.add_module(str(x), features[x])
|
||||
|
||||
for x in range(4, 7):
|
||||
self.relu2_1.add_module(str(x), features[x])
|
||||
|
||||
for x in range(7, 9):
|
||||
self.relu2_2.add_module(str(x), features[x])
|
||||
|
||||
for x in range(9, 12):
|
||||
self.relu3_1.add_module(str(x), features[x])
|
||||
|
||||
for x in range(12, 14):
|
||||
self.relu3_2.add_module(str(x), features[x])
|
||||
|
||||
for x in range(14, 16):
|
||||
self.relu3_2.add_module(str(x), features[x])
|
||||
|
||||
for x in range(16, 18):
|
||||
self.relu3_4.add_module(str(x), features[x])
|
||||
|
||||
for x in range(18, 21):
|
||||
self.relu4_1.add_module(str(x), features[x])
|
||||
|
||||
for x in range(21, 23):
|
||||
self.relu4_2.add_module(str(x), features[x])
|
||||
|
||||
for x in range(23, 25):
|
||||
self.relu4_3.add_module(str(x), features[x])
|
||||
|
||||
for x in range(25, 27):
|
||||
self.relu4_4.add_module(str(x), features[x])
|
||||
|
||||
for x in range(27, 30):
|
||||
self.relu5_1.add_module(str(x), features[x])
|
||||
|
||||
for x in range(30, 32):
|
||||
self.relu5_2.add_module(str(x), features[x])
|
||||
|
||||
for x in range(32, 34):
|
||||
self.relu5_3.add_module(str(x), features[x])
|
||||
|
||||
for x in range(34, 36):
|
||||
self.relu5_4.add_module(str(x), features[x])
|
||||
|
||||
# don't need the gradients, just want the features
|
||||
for param in self.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
def forward(self, x):
|
||||
relu1_1 = self.relu1_1(x)
|
||||
relu1_2 = self.relu1_2(relu1_1)
|
||||
|
||||
relu2_1 = self.relu2_1(relu1_2)
|
||||
relu2_2 = self.relu2_2(relu2_1)
|
||||
|
||||
relu3_1 = self.relu3_1(relu2_2)
|
||||
relu3_2 = self.relu3_2(relu3_1)
|
||||
relu3_3 = self.relu3_3(relu3_2)
|
||||
relu3_4 = self.relu3_4(relu3_3)
|
||||
|
||||
relu4_1 = self.relu4_1(relu3_4)
|
||||
relu4_2 = self.relu4_2(relu4_1)
|
||||
relu4_3 = self.relu4_3(relu4_2)
|
||||
relu4_4 = self.relu4_4(relu4_3)
|
||||
|
||||
relu5_1 = self.relu5_1(relu4_4)
|
||||
relu5_2 = self.relu5_2(relu5_1)
|
||||
relu5_3 = self.relu5_3(relu5_2)
|
||||
relu5_4 = self.relu5_4(relu5_3)
|
||||
|
||||
out = {
|
||||
'relu1_1': relu1_1,
|
||||
'relu1_2': relu1_2,
|
||||
|
||||
'relu2_1': relu2_1,
|
||||
'relu2_2': relu2_2,
|
||||
|
||||
'relu3_1': relu3_1,
|
||||
'relu3_2': relu3_2,
|
||||
'relu3_3': relu3_3,
|
||||
'relu3_4': relu3_4,
|
||||
|
||||
'relu4_1': relu4_1,
|
||||
'relu4_2': relu4_2,
|
||||
'relu4_3': relu4_3,
|
||||
'relu4_4': relu4_4,
|
||||
|
||||
'relu5_1': relu5_1,
|
||||
'relu5_2': relu5_2,
|
||||
'relu5_3': relu5_3,
|
||||
'relu5_4': relu5_4,
|
||||
}
|
||||
return out
|
||||
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
|
||||
from saicinpainting.training.modules.ffc import FFCResNetGenerator
|
||||
from saicinpainting.training.modules.pix2pixhd import GlobalGenerator, MultiDilatedGlobalGenerator, \
|
||||
NLayerDiscriminator, MultidilatedNLayerDiscriminator
|
||||
|
||||
def make_generator(config, kind, **kwargs):
|
||||
logging.info(f'Make generator {kind}')
|
||||
|
||||
if kind == 'pix2pixhd_multidilated':
|
||||
return MultiDilatedGlobalGenerator(**kwargs)
|
||||
|
||||
if kind == 'pix2pixhd_global':
|
||||
return GlobalGenerator(**kwargs)
|
||||
|
||||
if kind == 'ffc_resnet':
|
||||
return FFCResNetGenerator(**kwargs)
|
||||
|
||||
raise ValueError(f'Unknown generator kind {kind}')
|
||||
|
||||
|
||||
def make_discriminator(kind, **kwargs):
|
||||
logging.info(f'Make discriminator {kind}')
|
||||
|
||||
if kind == 'pix2pixhd_nlayer_multidilated':
|
||||
return MultidilatedNLayerDiscriminator(**kwargs)
|
||||
|
||||
if kind == 'pix2pixhd_nlayer':
|
||||
return NLayerDiscriminator(**kwargs)
|
||||
|
||||
raise ValueError(f'Unknown discriminator kind {kind}')
|
||||
@@ -0,0 +1,80 @@
|
||||
import abc
|
||||
from typing import Tuple, List
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from saicinpainting.training.modules.depthwise_sep_conv import DepthWiseSeperableConv
|
||||
from saicinpainting.training.modules.multidilated_conv import MultidilatedConv
|
||||
|
||||
|
||||
class BaseDiscriminator(nn.Module):
|
||||
@abc.abstractmethod
|
||||
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]:
|
||||
"""
|
||||
Predict scores and get intermediate activations. Useful for feature matching loss
|
||||
:return tuple (scores, list of intermediate activations)
|
||||
"""
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
def get_conv_block_ctor(kind='default'):
|
||||
if not isinstance(kind, str):
|
||||
return kind
|
||||
if kind == 'default':
|
||||
return nn.Conv2d
|
||||
if kind == 'depthwise':
|
||||
return DepthWiseSeperableConv
|
||||
if kind == 'multidilated':
|
||||
return MultidilatedConv
|
||||
raise ValueError(f'Unknown convolutional block kind {kind}')
|
||||
|
||||
|
||||
def get_norm_layer(kind='bn'):
|
||||
if not isinstance(kind, str):
|
||||
return kind
|
||||
if kind == 'bn':
|
||||
return nn.BatchNorm2d
|
||||
if kind == 'in':
|
||||
return nn.InstanceNorm2d
|
||||
raise ValueError(f'Unknown norm block kind {kind}')
|
||||
|
||||
|
||||
def get_activation(kind='tanh'):
|
||||
if kind == 'tanh':
|
||||
return nn.Tanh()
|
||||
if kind == 'sigmoid':
|
||||
return nn.Sigmoid()
|
||||
if kind is False:
|
||||
return nn.Identity()
|
||||
raise ValueError(f'Unknown activation kind {kind}')
|
||||
|
||||
|
||||
class SimpleMultiStepGenerator(nn.Module):
|
||||
def __init__(self, steps: List[nn.Module]):
|
||||
super().__init__()
|
||||
self.steps = nn.ModuleList(steps)
|
||||
|
||||
def forward(self, x):
|
||||
cur_in = x
|
||||
outs = []
|
||||
for step in self.steps:
|
||||
cur_out = step(cur_in)
|
||||
outs.append(cur_out)
|
||||
cur_in = torch.cat((cur_in, cur_out), dim=1)
|
||||
return torch.cat(outs[::-1], dim=1)
|
||||
|
||||
def deconv_factory(kind, ngf, mult, norm_layer, activation, max_features):
|
||||
if kind == 'convtranspose':
|
||||
return [nn.ConvTranspose2d(min(max_features, ngf * mult),
|
||||
min(max_features, int(ngf * mult / 2)),
|
||||
kernel_size=3, stride=2, padding=1, output_padding=1),
|
||||
norm_layer(min(max_features, int(ngf * mult / 2))), activation]
|
||||
elif kind == 'bilinear':
|
||||
return [nn.Upsample(scale_factor=2, mode='bilinear'),
|
||||
DepthWiseSeperableConv(min(max_features, ngf * mult),
|
||||
min(max_features, int(ngf * mult / 2)),
|
||||
kernel_size=3, stride=1, padding=1),
|
||||
norm_layer(min(max_features, int(ngf * mult / 2))), activation]
|
||||
else:
|
||||
raise Exception(f"Invalid deconv kind: {kind}")
|
||||
@@ -0,0 +1,17 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
class DepthWiseSeperableConv(nn.Module):
|
||||
def __init__(self, in_dim, out_dim, *args, **kwargs):
|
||||
super().__init__()
|
||||
if 'groups' in kwargs:
|
||||
# ignoring groups for Depthwise Sep Conv
|
||||
del kwargs['groups']
|
||||
|
||||
self.depthwise = nn.Conv2d(in_dim, in_dim, *args, groups=in_dim, **kwargs)
|
||||
self.pointwise = nn.Conv2d(in_dim, out_dim, kernel_size=1)
|
||||
|
||||
def forward(self, x):
|
||||
out = self.depthwise(x)
|
||||
out = self.pointwise(out)
|
||||
return out
|
||||
@@ -0,0 +1,47 @@
|
||||
import torch
|
||||
from kornia import SamplePadding
|
||||
from kornia.augmentation import RandomAffine, CenterCrop
|
||||
|
||||
|
||||
class FakeFakesGenerator:
|
||||
def __init__(self, aug_proba=0.5, img_aug_degree=30, img_aug_translate=0.2):
|
||||
self.grad_aug = RandomAffine(degrees=360,
|
||||
translate=0.2,
|
||||
padding_mode=SamplePadding.REFLECTION,
|
||||
keepdim=False,
|
||||
p=1)
|
||||
self.img_aug = RandomAffine(degrees=img_aug_degree,
|
||||
translate=img_aug_translate,
|
||||
padding_mode=SamplePadding.REFLECTION,
|
||||
keepdim=True,
|
||||
p=1)
|
||||
self.aug_proba = aug_proba
|
||||
|
||||
def __call__(self, input_images, masks):
|
||||
blend_masks = self._fill_masks_with_gradient(masks)
|
||||
blend_target = self._make_blend_target(input_images)
|
||||
result = input_images * (1 - blend_masks) + blend_target * blend_masks
|
||||
return result, blend_masks
|
||||
|
||||
def _make_blend_target(self, input_images):
|
||||
batch_size = input_images.shape[0]
|
||||
permuted = input_images[torch.randperm(batch_size)]
|
||||
augmented = self.img_aug(input_images)
|
||||
is_aug = (torch.rand(batch_size, device=input_images.device)[:, None, None, None] < self.aug_proba).float()
|
||||
result = augmented * is_aug + permuted * (1 - is_aug)
|
||||
return result
|
||||
|
||||
def _fill_masks_with_gradient(self, masks):
|
||||
batch_size, _, height, width = masks.shape
|
||||
grad = torch.linspace(0, 1, steps=width * 2, device=masks.device, dtype=masks.dtype) \
|
||||
.view(1, 1, 1, -1).expand(batch_size, 1, height * 2, width * 2)
|
||||
grad = self.grad_aug(grad)
|
||||
grad = CenterCrop((height, width))(grad)
|
||||
grad *= masks
|
||||
|
||||
grad_for_min = grad + (1 - masks) * 10
|
||||
grad -= grad_for_min.view(batch_size, -1).min(-1).values[:, None, None, None]
|
||||
grad /= grad.view(batch_size, -1).max(-1).values[:, None, None, None] + 1e-6
|
||||
grad.clamp_(min=0, max=1)
|
||||
|
||||
return grad
|
||||
@@ -0,0 +1,433 @@
|
||||
# Fast Fourier Convolution NeurIPS 2020
|
||||
# original implementation https://github.com/pkumivision/FFC/blob/main/model_zoo/ffc.py
|
||||
# paper https://proceedings.neurips.cc/paper/2020/file/2fd5d41ec6cfab47e32164d5624269b1-Paper.pdf
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from saicinpainting.training.modules.base import get_activation, BaseDiscriminator
|
||||
from saicinpainting.training.modules.spatial_transform import LearnableSpatialTransformWrapper
|
||||
from saicinpainting.training.modules.squeeze_excitation import SELayer
|
||||
from saicinpainting.utils import get_shape
|
||||
|
||||
|
||||
class FFCSE_block(nn.Module):
|
||||
|
||||
def __init__(self, channels, ratio_g):
|
||||
super(FFCSE_block, self).__init__()
|
||||
in_cg = int(channels * ratio_g)
|
||||
in_cl = channels - in_cg
|
||||
r = 16
|
||||
|
||||
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
|
||||
self.conv1 = nn.Conv2d(channels, channels // r,
|
||||
kernel_size=1, bias=True)
|
||||
self.relu1 = nn.ReLU(inplace=True)
|
||||
self.conv_a2l = None if in_cl == 0 else nn.Conv2d(
|
||||
channels // r, in_cl, kernel_size=1, bias=True)
|
||||
self.conv_a2g = None if in_cg == 0 else nn.Conv2d(
|
||||
channels // r, in_cg, kernel_size=1, bias=True)
|
||||
self.sigmoid = nn.Sigmoid()
|
||||
|
||||
def forward(self, x):
|
||||
x = x if type(x) is tuple else (x, 0)
|
||||
id_l, id_g = x
|
||||
|
||||
x = id_l if type(id_g) is int else torch.cat([id_l, id_g], dim=1)
|
||||
x = self.avgpool(x)
|
||||
x = self.relu1(self.conv1(x))
|
||||
|
||||
x_l = 0 if self.conv_a2l is None else id_l * \
|
||||
self.sigmoid(self.conv_a2l(x))
|
||||
x_g = 0 if self.conv_a2g is None else id_g * \
|
||||
self.sigmoid(self.conv_a2g(x))
|
||||
return x_l, x_g
|
||||
|
||||
|
||||
class FourierUnit(nn.Module):
|
||||
|
||||
def __init__(self, in_channels, out_channels, groups=1, spatial_scale_factor=None, spatial_scale_mode='bilinear',
|
||||
spectral_pos_encoding=False, use_se=False, se_kwargs=None, ffc3d=False, fft_norm='ortho'):
|
||||
# bn_layer not used
|
||||
super(FourierUnit, self).__init__()
|
||||
self.groups = groups
|
||||
|
||||
self.conv_layer = torch.nn.Conv2d(in_channels=in_channels * 2 + (2 if spectral_pos_encoding else 0),
|
||||
out_channels=out_channels * 2,
|
||||
kernel_size=1, stride=1, padding=0, groups=self.groups, bias=False)
|
||||
self.bn = torch.nn.BatchNorm2d(out_channels * 2)
|
||||
self.relu = torch.nn.ReLU(inplace=True)
|
||||
|
||||
# squeeze and excitation block
|
||||
self.use_se = use_se
|
||||
if use_se:
|
||||
if se_kwargs is None:
|
||||
se_kwargs = {}
|
||||
self.se = SELayer(self.conv_layer.in_channels, **se_kwargs)
|
||||
|
||||
self.spatial_scale_factor = spatial_scale_factor
|
||||
self.spatial_scale_mode = spatial_scale_mode
|
||||
self.spectral_pos_encoding = spectral_pos_encoding
|
||||
self.ffc3d = ffc3d
|
||||
self.fft_norm = fft_norm
|
||||
|
||||
def forward(self, x):
|
||||
batch = x.shape[0]
|
||||
|
||||
if self.spatial_scale_factor is not None:
|
||||
orig_size = x.shape[-2:]
|
||||
x = F.interpolate(x, scale_factor=self.spatial_scale_factor, mode=self.spatial_scale_mode, align_corners=False)
|
||||
|
||||
r_size = x.size()
|
||||
# (batch, c, h, w/2+1, 2)
|
||||
fft_dim = (-3, -2, -1) if self.ffc3d else (-2, -1)
|
||||
ffted = torch.fft.rfftn(x, dim=fft_dim, norm=self.fft_norm)
|
||||
ffted = torch.stack((ffted.real, ffted.imag), dim=-1)
|
||||
ffted = ffted.permute(0, 1, 4, 2, 3).contiguous() # (batch, c, 2, h, w/2+1)
|
||||
ffted = ffted.view((batch, -1,) + ffted.size()[3:])
|
||||
|
||||
if self.spectral_pos_encoding:
|
||||
height, width = ffted.shape[-2:]
|
||||
coords_vert = torch.linspace(0, 1, height)[None, None, :, None].expand(batch, 1, height, width).to(ffted)
|
||||
coords_hor = torch.linspace(0, 1, width)[None, None, None, :].expand(batch, 1, height, width).to(ffted)
|
||||
ffted = torch.cat((coords_vert, coords_hor, ffted), dim=1)
|
||||
|
||||
if self.use_se:
|
||||
ffted = self.se(ffted)
|
||||
|
||||
ffted = self.conv_layer(ffted) # (batch, c*2, h, w/2+1)
|
||||
ffted = self.relu(self.bn(ffted))
|
||||
|
||||
ffted = ffted.view((batch, -1, 2,) + ffted.size()[2:]).permute(
|
||||
0, 1, 3, 4, 2).contiguous() # (batch,c, t, h, w/2+1, 2)
|
||||
ffted = torch.complex(ffted[..., 0], ffted[..., 1])
|
||||
|
||||
ifft_shape_slice = x.shape[-3:] if self.ffc3d else x.shape[-2:]
|
||||
output = torch.fft.irfftn(ffted, s=ifft_shape_slice, dim=fft_dim, norm=self.fft_norm)
|
||||
|
||||
if self.spatial_scale_factor is not None:
|
||||
output = F.interpolate(output, size=orig_size, mode=self.spatial_scale_mode, align_corners=False)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class SpectralTransform(nn.Module):
|
||||
|
||||
def __init__(self, in_channels, out_channels, stride=1, groups=1, enable_lfu=True, **fu_kwargs):
|
||||
# bn_layer not used
|
||||
super(SpectralTransform, self).__init__()
|
||||
self.enable_lfu = enable_lfu
|
||||
if stride == 2:
|
||||
self.downsample = nn.AvgPool2d(kernel_size=(2, 2), stride=2)
|
||||
else:
|
||||
self.downsample = nn.Identity()
|
||||
|
||||
self.stride = stride
|
||||
self.conv1 = nn.Sequential(
|
||||
nn.Conv2d(in_channels, out_channels //
|
||||
2, kernel_size=1, groups=groups, bias=False),
|
||||
nn.BatchNorm2d(out_channels // 2),
|
||||
nn.ReLU(inplace=True)
|
||||
)
|
||||
self.fu = FourierUnit(
|
||||
out_channels // 2, out_channels // 2, groups, **fu_kwargs)
|
||||
if self.enable_lfu:
|
||||
self.lfu = FourierUnit(
|
||||
out_channels // 2, out_channels // 2, groups)
|
||||
self.conv2 = torch.nn.Conv2d(
|
||||
out_channels // 2, out_channels, kernel_size=1, groups=groups, bias=False)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
x = self.downsample(x)
|
||||
x = self.conv1(x)
|
||||
output = self.fu(x)
|
||||
|
||||
if self.enable_lfu:
|
||||
n, c, h, w = x.shape
|
||||
split_no = 2
|
||||
split_s = h // split_no
|
||||
xs = torch.cat(torch.split(
|
||||
x[:, :c // 4], split_s, dim=-2), dim=1).contiguous()
|
||||
xs = torch.cat(torch.split(xs, split_s, dim=-1),
|
||||
dim=1).contiguous()
|
||||
xs = self.lfu(xs)
|
||||
xs = xs.repeat(1, 1, split_no, split_no).contiguous()
|
||||
else:
|
||||
xs = 0
|
||||
|
||||
output = self.conv2(x + output + xs)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class FFC(nn.Module):
|
||||
|
||||
def __init__(self, in_channels, out_channels, kernel_size,
|
||||
ratio_gin, ratio_gout, stride=1, padding=0,
|
||||
dilation=1, groups=1, bias=False, enable_lfu=True,
|
||||
padding_type='reflect', gated=False, **spectral_kwargs):
|
||||
super(FFC, self).__init__()
|
||||
|
||||
assert stride == 1 or stride == 2, "Stride should be 1 or 2."
|
||||
self.stride = stride
|
||||
|
||||
in_cg = int(in_channels * ratio_gin)
|
||||
in_cl = in_channels - in_cg
|
||||
out_cg = int(out_channels * ratio_gout)
|
||||
out_cl = out_channels - out_cg
|
||||
#groups_g = 1 if groups == 1 else int(groups * ratio_gout)
|
||||
#groups_l = 1 if groups == 1 else groups - groups_g
|
||||
|
||||
self.ratio_gin = ratio_gin
|
||||
self.ratio_gout = ratio_gout
|
||||
self.global_in_num = in_cg
|
||||
|
||||
module = nn.Identity if in_cl == 0 or out_cl == 0 else nn.Conv2d
|
||||
self.convl2l = module(in_cl, out_cl, kernel_size,
|
||||
stride, padding, dilation, groups, bias, padding_mode=padding_type)
|
||||
module = nn.Identity if in_cl == 0 or out_cg == 0 else nn.Conv2d
|
||||
self.convl2g = module(in_cl, out_cg, kernel_size,
|
||||
stride, padding, dilation, groups, bias, padding_mode=padding_type)
|
||||
module = nn.Identity if in_cg == 0 or out_cl == 0 else nn.Conv2d
|
||||
self.convg2l = module(in_cg, out_cl, kernel_size,
|
||||
stride, padding, dilation, groups, bias, padding_mode=padding_type)
|
||||
module = nn.Identity if in_cg == 0 or out_cg == 0 else SpectralTransform
|
||||
self.convg2g = module(
|
||||
in_cg, out_cg, stride, 1 if groups == 1 else groups // 2, enable_lfu, **spectral_kwargs)
|
||||
|
||||
self.gated = gated
|
||||
module = nn.Identity if in_cg == 0 or out_cl == 0 or not self.gated else nn.Conv2d
|
||||
self.gate = module(in_channels, 2, 1)
|
||||
|
||||
def forward(self, x):
|
||||
x_l, x_g = x if type(x) is tuple else (x, 0)
|
||||
out_xl, out_xg = 0, 0
|
||||
|
||||
if self.gated:
|
||||
total_input_parts = [x_l]
|
||||
if torch.is_tensor(x_g):
|
||||
total_input_parts.append(x_g)
|
||||
total_input = torch.cat(total_input_parts, dim=1)
|
||||
|
||||
gates = torch.sigmoid(self.gate(total_input))
|
||||
g2l_gate, l2g_gate = gates.chunk(2, dim=1)
|
||||
else:
|
||||
g2l_gate, l2g_gate = 1, 1
|
||||
|
||||
if self.ratio_gout != 1:
|
||||
out_xl = self.convl2l(x_l) + self.convg2l(x_g) * g2l_gate
|
||||
if self.ratio_gout != 0:
|
||||
out_xg = self.convl2g(x_l) * l2g_gate + self.convg2g(x_g)
|
||||
|
||||
return out_xl, out_xg
|
||||
|
||||
|
||||
class FFC_BN_ACT(nn.Module):
|
||||
|
||||
def __init__(self, in_channels, out_channels,
|
||||
kernel_size, ratio_gin, ratio_gout,
|
||||
stride=1, padding=0, dilation=1, groups=1, bias=False,
|
||||
norm_layer=nn.BatchNorm2d, activation_layer=nn.Identity,
|
||||
padding_type='reflect',
|
||||
enable_lfu=True, **kwargs):
|
||||
super(FFC_BN_ACT, self).__init__()
|
||||
self.ffc = FFC(in_channels, out_channels, kernel_size,
|
||||
ratio_gin, ratio_gout, stride, padding, dilation,
|
||||
groups, bias, enable_lfu, padding_type=padding_type, **kwargs)
|
||||
lnorm = nn.Identity if ratio_gout == 1 else norm_layer
|
||||
gnorm = nn.Identity if ratio_gout == 0 else norm_layer
|
||||
global_channels = int(out_channels * ratio_gout)
|
||||
self.bn_l = lnorm(out_channels - global_channels)
|
||||
self.bn_g = gnorm(global_channels)
|
||||
|
||||
lact = nn.Identity if ratio_gout == 1 else activation_layer
|
||||
gact = nn.Identity if ratio_gout == 0 else activation_layer
|
||||
self.act_l = lact(inplace=True)
|
||||
self.act_g = gact(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
x_l, x_g = self.ffc(x)
|
||||
x_l = self.act_l(self.bn_l(x_l))
|
||||
x_g = self.act_g(self.bn_g(x_g))
|
||||
return x_l, x_g
|
||||
|
||||
|
||||
class FFCResnetBlock(nn.Module):
|
||||
def __init__(self, dim, padding_type, norm_layer, activation_layer=nn.ReLU, dilation=1,
|
||||
spatial_transform_kwargs=None, inline=False, **conv_kwargs):
|
||||
super().__init__()
|
||||
self.conv1 = FFC_BN_ACT(dim, dim, kernel_size=3, padding=dilation, dilation=dilation,
|
||||
norm_layer=norm_layer,
|
||||
activation_layer=activation_layer,
|
||||
padding_type=padding_type,
|
||||
**conv_kwargs)
|
||||
self.conv2 = FFC_BN_ACT(dim, dim, kernel_size=3, padding=dilation, dilation=dilation,
|
||||
norm_layer=norm_layer,
|
||||
activation_layer=activation_layer,
|
||||
padding_type=padding_type,
|
||||
**conv_kwargs)
|
||||
if spatial_transform_kwargs is not None:
|
||||
self.conv1 = LearnableSpatialTransformWrapper(self.conv1, **spatial_transform_kwargs)
|
||||
self.conv2 = LearnableSpatialTransformWrapper(self.conv2, **spatial_transform_kwargs)
|
||||
self.inline = inline
|
||||
|
||||
def forward(self, x):
|
||||
if self.inline:
|
||||
x_l, x_g = x[:, :-self.conv1.ffc.global_in_num], x[:, -self.conv1.ffc.global_in_num:]
|
||||
else:
|
||||
x_l, x_g = x if type(x) is tuple else (x, 0)
|
||||
|
||||
id_l, id_g = x_l, x_g
|
||||
|
||||
x_l, x_g = self.conv1((x_l, x_g))
|
||||
x_l, x_g = self.conv2((x_l, x_g))
|
||||
|
||||
x_l, x_g = id_l + x_l, id_g + x_g
|
||||
out = x_l, x_g
|
||||
if self.inline:
|
||||
out = torch.cat(out, dim=1)
|
||||
return out
|
||||
|
||||
|
||||
class ConcatTupleLayer(nn.Module):
|
||||
def forward(self, x):
|
||||
assert isinstance(x, tuple)
|
||||
x_l, x_g = x
|
||||
assert torch.is_tensor(x_l) or torch.is_tensor(x_g)
|
||||
if not torch.is_tensor(x_g):
|
||||
return x_l
|
||||
return torch.cat(x, dim=1)
|
||||
|
||||
|
||||
class FFCResNetGenerator(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, ngf=64, n_downsampling=3, n_blocks=9, norm_layer=nn.BatchNorm2d,
|
||||
padding_type='reflect', activation_layer=nn.ReLU,
|
||||
up_norm_layer=nn.BatchNorm2d, up_activation=nn.ReLU(True),
|
||||
init_conv_kwargs={}, downsample_conv_kwargs={}, resnet_conv_kwargs={},
|
||||
spatial_transform_layers=None, spatial_transform_kwargs={},
|
||||
add_out_act=True, max_features=1024, out_ffc=False, out_ffc_kwargs={}):
|
||||
assert (n_blocks >= 0)
|
||||
super().__init__()
|
||||
|
||||
model = [nn.ReflectionPad2d(3),
|
||||
FFC_BN_ACT(input_nc, ngf, kernel_size=7, padding=0, norm_layer=norm_layer,
|
||||
activation_layer=activation_layer, **init_conv_kwargs)]
|
||||
|
||||
### downsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** i
|
||||
if i == n_downsampling - 1:
|
||||
cur_conv_kwargs = dict(downsample_conv_kwargs)
|
||||
cur_conv_kwargs['ratio_gout'] = resnet_conv_kwargs.get('ratio_gin', 0)
|
||||
else:
|
||||
cur_conv_kwargs = downsample_conv_kwargs
|
||||
model += [FFC_BN_ACT(min(max_features, ngf * mult),
|
||||
min(max_features, ngf * mult * 2),
|
||||
kernel_size=3, stride=2, padding=1,
|
||||
norm_layer=norm_layer,
|
||||
activation_layer=activation_layer,
|
||||
**cur_conv_kwargs)]
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
feats_num_bottleneck = min(max_features, ngf * mult)
|
||||
|
||||
### resnet blocks
|
||||
for i in range(n_blocks):
|
||||
cur_resblock = FFCResnetBlock(feats_num_bottleneck, padding_type=padding_type, activation_layer=activation_layer,
|
||||
norm_layer=norm_layer, **resnet_conv_kwargs)
|
||||
if spatial_transform_layers is not None and i in spatial_transform_layers:
|
||||
cur_resblock = LearnableSpatialTransformWrapper(cur_resblock, **spatial_transform_kwargs)
|
||||
model += [cur_resblock]
|
||||
|
||||
model += [ConcatTupleLayer()]
|
||||
|
||||
### upsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** (n_downsampling - i)
|
||||
model += [nn.ConvTranspose2d(min(max_features, ngf * mult),
|
||||
min(max_features, int(ngf * mult / 2)),
|
||||
kernel_size=3, stride=2, padding=1, output_padding=1),
|
||||
up_norm_layer(min(max_features, int(ngf * mult / 2))),
|
||||
up_activation]
|
||||
|
||||
if out_ffc:
|
||||
model += [FFCResnetBlock(ngf, padding_type=padding_type, activation_layer=activation_layer,
|
||||
norm_layer=norm_layer, inline=True, **out_ffc_kwargs)]
|
||||
|
||||
model += [nn.ReflectionPad2d(3),
|
||||
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
|
||||
if add_out_act:
|
||||
model.append(get_activation('tanh' if add_out_act is True else add_out_act))
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def forward(self, input):
|
||||
return self.model(input)
|
||||
|
||||
|
||||
class FFCNLayerDiscriminator(BaseDiscriminator):
|
||||
def __init__(self, input_nc, ndf=64, n_layers=3, norm_layer=nn.BatchNorm2d, max_features=512,
|
||||
init_conv_kwargs={}, conv_kwargs={}):
|
||||
super().__init__()
|
||||
self.n_layers = n_layers
|
||||
|
||||
def _act_ctor(inplace=True):
|
||||
return nn.LeakyReLU(negative_slope=0.2, inplace=inplace)
|
||||
|
||||
kw = 3
|
||||
padw = int(np.ceil((kw-1.0)/2))
|
||||
sequence = [[FFC_BN_ACT(input_nc, ndf, kernel_size=kw, padding=padw, norm_layer=norm_layer,
|
||||
activation_layer=_act_ctor, **init_conv_kwargs)]]
|
||||
|
||||
nf = ndf
|
||||
for n in range(1, n_layers):
|
||||
nf_prev = nf
|
||||
nf = min(nf * 2, max_features)
|
||||
|
||||
cur_model = [
|
||||
FFC_BN_ACT(nf_prev, nf,
|
||||
kernel_size=kw, stride=2, padding=padw,
|
||||
norm_layer=norm_layer,
|
||||
activation_layer=_act_ctor,
|
||||
**conv_kwargs)
|
||||
]
|
||||
sequence.append(cur_model)
|
||||
|
||||
nf_prev = nf
|
||||
nf = min(nf * 2, 512)
|
||||
|
||||
cur_model = [
|
||||
FFC_BN_ACT(nf_prev, nf,
|
||||
kernel_size=kw, stride=1, padding=padw,
|
||||
norm_layer=norm_layer,
|
||||
activation_layer=lambda *args, **kwargs: nn.LeakyReLU(*args, negative_slope=0.2, **kwargs),
|
||||
**conv_kwargs),
|
||||
ConcatTupleLayer()
|
||||
]
|
||||
sequence.append(cur_model)
|
||||
|
||||
sequence += [[nn.Conv2d(nf, 1, kernel_size=kw, stride=1, padding=padw)]]
|
||||
|
||||
for n in range(len(sequence)):
|
||||
setattr(self, 'model'+str(n), nn.Sequential(*sequence[n]))
|
||||
|
||||
def get_all_activations(self, x):
|
||||
res = [x]
|
||||
for n in range(self.n_layers + 2):
|
||||
model = getattr(self, 'model' + str(n))
|
||||
res.append(model(res[-1]))
|
||||
return res[1:]
|
||||
|
||||
def forward(self, x):
|
||||
act = self.get_all_activations(x)
|
||||
feats = []
|
||||
for out in act[:-1]:
|
||||
if isinstance(out, tuple):
|
||||
if torch.is_tensor(out[1]):
|
||||
out = torch.cat(out, dim=1)
|
||||
else:
|
||||
out = out[0]
|
||||
feats.append(out)
|
||||
return act[-1], feats
|
||||
@@ -0,0 +1,98 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import random
|
||||
from saicinpainting.training.modules.depthwise_sep_conv import DepthWiseSeperableConv
|
||||
|
||||
class MultidilatedConv(nn.Module):
|
||||
def __init__(self, in_dim, out_dim, kernel_size, dilation_num=3, comb_mode='sum', equal_dim=True,
|
||||
shared_weights=False, padding=1, min_dilation=1, shuffle_in_channels=False, use_depthwise=False, **kwargs):
|
||||
super().__init__()
|
||||
convs = []
|
||||
self.equal_dim = equal_dim
|
||||
assert comb_mode in ('cat_out', 'sum', 'cat_in', 'cat_both'), comb_mode
|
||||
if comb_mode in ('cat_out', 'cat_both'):
|
||||
self.cat_out = True
|
||||
if equal_dim:
|
||||
assert out_dim % dilation_num == 0
|
||||
out_dims = [out_dim // dilation_num] * dilation_num
|
||||
self.index = sum([[i + j * (out_dims[0]) for j in range(dilation_num)] for i in range(out_dims[0])], [])
|
||||
else:
|
||||
out_dims = [out_dim // 2 ** (i + 1) for i in range(dilation_num - 1)]
|
||||
out_dims.append(out_dim - sum(out_dims))
|
||||
index = []
|
||||
starts = [0] + out_dims[:-1]
|
||||
lengths = [out_dims[i] // out_dims[-1] for i in range(dilation_num)]
|
||||
for i in range(out_dims[-1]):
|
||||
for j in range(dilation_num):
|
||||
index += list(range(starts[j], starts[j] + lengths[j]))
|
||||
starts[j] += lengths[j]
|
||||
self.index = index
|
||||
assert(len(index) == out_dim)
|
||||
self.out_dims = out_dims
|
||||
else:
|
||||
self.cat_out = False
|
||||
self.out_dims = [out_dim] * dilation_num
|
||||
|
||||
if comb_mode in ('cat_in', 'cat_both'):
|
||||
if equal_dim:
|
||||
assert in_dim % dilation_num == 0
|
||||
in_dims = [in_dim // dilation_num] * dilation_num
|
||||
else:
|
||||
in_dims = [in_dim // 2 ** (i + 1) for i in range(dilation_num - 1)]
|
||||
in_dims.append(in_dim - sum(in_dims))
|
||||
self.in_dims = in_dims
|
||||
self.cat_in = True
|
||||
else:
|
||||
self.cat_in = False
|
||||
self.in_dims = [in_dim] * dilation_num
|
||||
|
||||
conv_type = DepthWiseSeperableConv if use_depthwise else nn.Conv2d
|
||||
dilation = min_dilation
|
||||
for i in range(dilation_num):
|
||||
if isinstance(padding, int):
|
||||
cur_padding = padding * dilation
|
||||
else:
|
||||
cur_padding = padding[i]
|
||||
convs.append(conv_type(
|
||||
self.in_dims[i], self.out_dims[i], kernel_size, padding=cur_padding, dilation=dilation, **kwargs
|
||||
))
|
||||
if i > 0 and shared_weights:
|
||||
convs[-1].weight = convs[0].weight
|
||||
convs[-1].bias = convs[0].bias
|
||||
dilation *= 2
|
||||
self.convs = nn.ModuleList(convs)
|
||||
|
||||
self.shuffle_in_channels = shuffle_in_channels
|
||||
if self.shuffle_in_channels:
|
||||
# shuffle list as shuffling of tensors is nondeterministic
|
||||
in_channels_permute = list(range(in_dim))
|
||||
random.shuffle(in_channels_permute)
|
||||
# save as buffer so it is saved and loaded with checkpoint
|
||||
self.register_buffer('in_channels_permute', torch.tensor(in_channels_permute))
|
||||
|
||||
def forward(self, x):
|
||||
if self.shuffle_in_channels:
|
||||
x = x[:, self.in_channels_permute]
|
||||
|
||||
outs = []
|
||||
if self.cat_in:
|
||||
if self.equal_dim:
|
||||
x = x.chunk(len(self.convs), dim=1)
|
||||
else:
|
||||
new_x = []
|
||||
start = 0
|
||||
for dim in self.in_dims:
|
||||
new_x.append(x[:, start:start+dim])
|
||||
start += dim
|
||||
x = new_x
|
||||
for i, conv in enumerate(self.convs):
|
||||
if self.cat_in:
|
||||
input = x[i]
|
||||
else:
|
||||
input = x
|
||||
outs.append(conv(input))
|
||||
if self.cat_out:
|
||||
out = torch.cat(outs, dim=1)[:, self.index]
|
||||
else:
|
||||
out = sum(outs)
|
||||
return out
|
||||
@@ -0,0 +1,244 @@
|
||||
from typing import List, Tuple, Union, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from saicinpainting.training.modules.base import get_conv_block_ctor, get_activation
|
||||
from saicinpainting.training.modules.pix2pixhd import ResnetBlock
|
||||
|
||||
|
||||
class ResNetHead(nn.Module):
|
||||
def __init__(self, input_nc, ngf=64, n_downsampling=3, n_blocks=9, norm_layer=nn.BatchNorm2d,
|
||||
padding_type='reflect', conv_kind='default', activation=nn.ReLU(True)):
|
||||
assert (n_blocks >= 0)
|
||||
super(ResNetHead, self).__init__()
|
||||
|
||||
conv_layer = get_conv_block_ctor(conv_kind)
|
||||
|
||||
model = [nn.ReflectionPad2d(3),
|
||||
conv_layer(input_nc, ngf, kernel_size=7, padding=0),
|
||||
norm_layer(ngf),
|
||||
activation]
|
||||
|
||||
### downsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** i
|
||||
model += [conv_layer(ngf * mult, ngf * mult * 2, kernel_size=3, stride=2, padding=1),
|
||||
norm_layer(ngf * mult * 2),
|
||||
activation]
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
|
||||
### resnet blocks
|
||||
for i in range(n_blocks):
|
||||
model += [ResnetBlock(ngf * mult, padding_type=padding_type, activation=activation, norm_layer=norm_layer,
|
||||
conv_kind=conv_kind)]
|
||||
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def forward(self, input):
|
||||
return self.model(input)
|
||||
|
||||
|
||||
class ResNetTail(nn.Module):
|
||||
def __init__(self, output_nc, ngf=64, n_downsampling=3, n_blocks=9, norm_layer=nn.BatchNorm2d,
|
||||
padding_type='reflect', conv_kind='default', activation=nn.ReLU(True),
|
||||
up_norm_layer=nn.BatchNorm2d, up_activation=nn.ReLU(True), add_out_act=False, out_extra_layers_n=0,
|
||||
add_in_proj=None):
|
||||
assert (n_blocks >= 0)
|
||||
super(ResNetTail, self).__init__()
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
|
||||
model = []
|
||||
|
||||
if add_in_proj is not None:
|
||||
model.append(nn.Conv2d(add_in_proj, ngf * mult, kernel_size=1))
|
||||
|
||||
### resnet blocks
|
||||
for i in range(n_blocks):
|
||||
model += [ResnetBlock(ngf * mult, padding_type=padding_type, activation=activation, norm_layer=norm_layer,
|
||||
conv_kind=conv_kind)]
|
||||
|
||||
### upsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** (n_downsampling - i)
|
||||
model += [nn.ConvTranspose2d(ngf * mult, int(ngf * mult / 2), kernel_size=3, stride=2, padding=1,
|
||||
output_padding=1),
|
||||
up_norm_layer(int(ngf * mult / 2)),
|
||||
up_activation]
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
out_layers = []
|
||||
for _ in range(out_extra_layers_n):
|
||||
out_layers += [nn.Conv2d(ngf, ngf, kernel_size=1, padding=0),
|
||||
up_norm_layer(ngf),
|
||||
up_activation]
|
||||
out_layers += [nn.ReflectionPad2d(3),
|
||||
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
|
||||
|
||||
if add_out_act:
|
||||
out_layers.append(get_activation('tanh' if add_out_act is True else add_out_act))
|
||||
|
||||
self.out_proj = nn.Sequential(*out_layers)
|
||||
|
||||
def forward(self, input, return_last_act=False):
|
||||
features = self.model(input)
|
||||
out = self.out_proj(features)
|
||||
if return_last_act:
|
||||
return out, features
|
||||
else:
|
||||
return out
|
||||
|
||||
|
||||
class MultiscaleResNet(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, ngf=64, n_downsampling=2, n_blocks_head=2, n_blocks_tail=6, n_scales=3,
|
||||
norm_layer=nn.BatchNorm2d, padding_type='reflect', conv_kind='default', activation=nn.ReLU(True),
|
||||
up_norm_layer=nn.BatchNorm2d, up_activation=nn.ReLU(True), add_out_act=False, out_extra_layers_n=0,
|
||||
out_cumulative=False, return_only_hr=False):
|
||||
super().__init__()
|
||||
|
||||
self.heads = nn.ModuleList([ResNetHead(input_nc, ngf=ngf, n_downsampling=n_downsampling,
|
||||
n_blocks=n_blocks_head, norm_layer=norm_layer, padding_type=padding_type,
|
||||
conv_kind=conv_kind, activation=activation)
|
||||
for i in range(n_scales)])
|
||||
tail_in_feats = ngf * (2 ** n_downsampling) + ngf
|
||||
self.tails = nn.ModuleList([ResNetTail(output_nc,
|
||||
ngf=ngf, n_downsampling=n_downsampling,
|
||||
n_blocks=n_blocks_tail, norm_layer=norm_layer, padding_type=padding_type,
|
||||
conv_kind=conv_kind, activation=activation, up_norm_layer=up_norm_layer,
|
||||
up_activation=up_activation, add_out_act=add_out_act,
|
||||
out_extra_layers_n=out_extra_layers_n,
|
||||
add_in_proj=None if (i == n_scales - 1) else tail_in_feats)
|
||||
for i in range(n_scales)])
|
||||
|
||||
self.out_cumulative = out_cumulative
|
||||
self.return_only_hr = return_only_hr
|
||||
|
||||
@property
|
||||
def num_scales(self):
|
||||
return len(self.heads)
|
||||
|
||||
def forward(self, ms_inputs: List[torch.Tensor], smallest_scales_num: Optional[int] = None) \
|
||||
-> Union[torch.Tensor, List[torch.Tensor]]:
|
||||
"""
|
||||
:param ms_inputs: List of inputs of different resolutions from HR to LR
|
||||
:param smallest_scales_num: int or None, number of smallest scales to take at input
|
||||
:return: Depending on return_only_hr:
|
||||
True: Only the most HR output
|
||||
False: List of outputs of different resolutions from HR to LR
|
||||
"""
|
||||
if smallest_scales_num is None:
|
||||
assert len(self.heads) == len(ms_inputs), (len(self.heads), len(ms_inputs), smallest_scales_num)
|
||||
smallest_scales_num = len(self.heads)
|
||||
else:
|
||||
assert smallest_scales_num == len(ms_inputs) <= len(self.heads), (len(self.heads), len(ms_inputs), smallest_scales_num)
|
||||
|
||||
cur_heads = self.heads[-smallest_scales_num:]
|
||||
ms_features = [cur_head(cur_inp) for cur_head, cur_inp in zip(cur_heads, ms_inputs)]
|
||||
|
||||
all_outputs = []
|
||||
prev_tail_features = None
|
||||
for i in range(len(ms_features)):
|
||||
scale_i = -i - 1
|
||||
|
||||
cur_tail_input = ms_features[-i - 1]
|
||||
if prev_tail_features is not None:
|
||||
if prev_tail_features.shape != cur_tail_input.shape:
|
||||
prev_tail_features = F.interpolate(prev_tail_features, size=cur_tail_input.shape[2:],
|
||||
mode='bilinear', align_corners=False)
|
||||
cur_tail_input = torch.cat((cur_tail_input, prev_tail_features), dim=1)
|
||||
|
||||
cur_out, cur_tail_feats = self.tails[scale_i](cur_tail_input, return_last_act=True)
|
||||
|
||||
prev_tail_features = cur_tail_feats
|
||||
all_outputs.append(cur_out)
|
||||
|
||||
if self.out_cumulative:
|
||||
all_outputs_cum = [all_outputs[0]]
|
||||
for i in range(1, len(ms_features)):
|
||||
cur_out = all_outputs[i]
|
||||
cur_out_cum = cur_out + F.interpolate(all_outputs_cum[-1], size=cur_out.shape[2:],
|
||||
mode='bilinear', align_corners=False)
|
||||
all_outputs_cum.append(cur_out_cum)
|
||||
all_outputs = all_outputs_cum
|
||||
|
||||
if self.return_only_hr:
|
||||
return all_outputs[-1]
|
||||
else:
|
||||
return all_outputs[::-1]
|
||||
|
||||
|
||||
class MultiscaleDiscriminatorSimple(nn.Module):
|
||||
def __init__(self, ms_impl):
|
||||
super().__init__()
|
||||
self.ms_impl = nn.ModuleList(ms_impl)
|
||||
|
||||
@property
|
||||
def num_scales(self):
|
||||
return len(self.ms_impl)
|
||||
|
||||
def forward(self, ms_inputs: List[torch.Tensor], smallest_scales_num: Optional[int] = None) \
|
||||
-> List[Tuple[torch.Tensor, List[torch.Tensor]]]:
|
||||
"""
|
||||
:param ms_inputs: List of inputs of different resolutions from HR to LR
|
||||
:param smallest_scales_num: int or None, number of smallest scales to take at input
|
||||
:return: List of pairs (prediction, features) for different resolutions from HR to LR
|
||||
"""
|
||||
if smallest_scales_num is None:
|
||||
assert len(self.ms_impl) == len(ms_inputs), (len(self.ms_impl), len(ms_inputs), smallest_scales_num)
|
||||
smallest_scales_num = len(self.heads)
|
||||
else:
|
||||
assert smallest_scales_num == len(ms_inputs) <= len(self.ms_impl), \
|
||||
(len(self.ms_impl), len(ms_inputs), smallest_scales_num)
|
||||
|
||||
return [cur_discr(cur_input) for cur_discr, cur_input in zip(self.ms_impl[-smallest_scales_num:], ms_inputs)]
|
||||
|
||||
|
||||
class SingleToMultiScaleInputMixin:
|
||||
def forward(self, x: torch.Tensor) -> List:
|
||||
orig_height, orig_width = x.shape[2:]
|
||||
factors = [2 ** i for i in range(self.num_scales)]
|
||||
ms_inputs = [F.interpolate(x, size=(orig_height // f, orig_width // f), mode='bilinear', align_corners=False)
|
||||
for f in factors]
|
||||
return super().forward(ms_inputs)
|
||||
|
||||
|
||||
class GeneratorMultiToSingleOutputMixin:
|
||||
def forward(self, x):
|
||||
return super().forward(x)[0]
|
||||
|
||||
|
||||
class DiscriminatorMultiToSingleOutputMixin:
|
||||
def forward(self, x):
|
||||
out_feat_tuples = super().forward(x)
|
||||
return out_feat_tuples[0][0], [f for _, flist in out_feat_tuples for f in flist]
|
||||
|
||||
|
||||
class DiscriminatorMultiToSingleOutputStackedMixin:
|
||||
def __init__(self, *args, return_feats_only_levels=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.return_feats_only_levels = return_feats_only_levels
|
||||
|
||||
def forward(self, x):
|
||||
out_feat_tuples = super().forward(x)
|
||||
outs = [out for out, _ in out_feat_tuples]
|
||||
scaled_outs = [outs[0]] + [F.interpolate(cur_out, size=outs[0].shape[-2:],
|
||||
mode='bilinear', align_corners=False)
|
||||
for cur_out in outs[1:]]
|
||||
out = torch.cat(scaled_outs, dim=1)
|
||||
if self.return_feats_only_levels is not None:
|
||||
feat_lists = [out_feat_tuples[i][1] for i in self.return_feats_only_levels]
|
||||
else:
|
||||
feat_lists = [flist for _, flist in out_feat_tuples]
|
||||
feats = [f for flist in feat_lists for f in flist]
|
||||
return out, feats
|
||||
|
||||
|
||||
class MultiscaleDiscrSingleInput(SingleToMultiScaleInputMixin, DiscriminatorMultiToSingleOutputStackedMixin, MultiscaleDiscriminatorSimple):
|
||||
pass
|
||||
|
||||
|
||||
class MultiscaleResNetSingle(GeneratorMultiToSingleOutputMixin, SingleToMultiScaleInputMixin, MultiscaleResNet):
|
||||
pass
|
||||
@@ -0,0 +1,669 @@
|
||||
# original: https://github.com/NVIDIA/pix2pixHD/blob/master/models/networks.py
|
||||
import collections
|
||||
from functools import partial
|
||||
import functools
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import torch.nn as nn
|
||||
|
||||
from saicinpainting.training.modules.base import BaseDiscriminator, deconv_factory, get_conv_block_ctor, get_norm_layer, get_activation
|
||||
from saicinpainting.training.modules.ffc import FFCResnetBlock
|
||||
from saicinpainting.training.modules.multidilated_conv import MultidilatedConv
|
||||
|
||||
class DotDict(defaultdict):
|
||||
# https://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary
|
||||
"""dot.notation access to dictionary attributes"""
|
||||
__getattr__ = defaultdict.get
|
||||
__setattr__ = defaultdict.__setitem__
|
||||
__delattr__ = defaultdict.__delitem__
|
||||
|
||||
class Identity(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, x):
|
||||
return x
|
||||
|
||||
|
||||
class ResnetBlock(nn.Module):
|
||||
def __init__(self, dim, padding_type, norm_layer, activation=nn.ReLU(True), use_dropout=False, conv_kind='default',
|
||||
dilation=1, in_dim=None, groups=1, second_dilation=None):
|
||||
super(ResnetBlock, self).__init__()
|
||||
self.in_dim = in_dim
|
||||
self.dim = dim
|
||||
if second_dilation is None:
|
||||
second_dilation = dilation
|
||||
self.conv_block = self.build_conv_block(dim, padding_type, norm_layer, activation, use_dropout,
|
||||
conv_kind=conv_kind, dilation=dilation, in_dim=in_dim, groups=groups,
|
||||
second_dilation=second_dilation)
|
||||
|
||||
if self.in_dim is not None:
|
||||
self.input_conv = nn.Conv2d(in_dim, dim, 1)
|
||||
|
||||
self.out_channnels = dim
|
||||
|
||||
def build_conv_block(self, dim, padding_type, norm_layer, activation, use_dropout, conv_kind='default',
|
||||
dilation=1, in_dim=None, groups=1, second_dilation=1):
|
||||
conv_layer = get_conv_block_ctor(conv_kind)
|
||||
|
||||
conv_block = []
|
||||
p = 0
|
||||
if padding_type == 'reflect':
|
||||
conv_block += [nn.ReflectionPad2d(dilation)]
|
||||
elif padding_type == 'replicate':
|
||||
conv_block += [nn.ReplicationPad2d(dilation)]
|
||||
elif padding_type == 'zero':
|
||||
p = dilation
|
||||
else:
|
||||
raise NotImplementedError('padding [%s] is not implemented' % padding_type)
|
||||
|
||||
if in_dim is None:
|
||||
in_dim = dim
|
||||
|
||||
conv_block += [conv_layer(in_dim, dim, kernel_size=3, padding=p, dilation=dilation),
|
||||
norm_layer(dim),
|
||||
activation]
|
||||
if use_dropout:
|
||||
conv_block += [nn.Dropout(0.5)]
|
||||
|
||||
p = 0
|
||||
if padding_type == 'reflect':
|
||||
conv_block += [nn.ReflectionPad2d(second_dilation)]
|
||||
elif padding_type == 'replicate':
|
||||
conv_block += [nn.ReplicationPad2d(second_dilation)]
|
||||
elif padding_type == 'zero':
|
||||
p = second_dilation
|
||||
else:
|
||||
raise NotImplementedError('padding [%s] is not implemented' % padding_type)
|
||||
conv_block += [conv_layer(dim, dim, kernel_size=3, padding=p, dilation=second_dilation, groups=groups),
|
||||
norm_layer(dim)]
|
||||
|
||||
return nn.Sequential(*conv_block)
|
||||
|
||||
def forward(self, x):
|
||||
x_before = x
|
||||
if self.in_dim is not None:
|
||||
x = self.input_conv(x)
|
||||
out = x + self.conv_block(x_before)
|
||||
return out
|
||||
|
||||
class ResnetBlock5x5(nn.Module):
|
||||
def __init__(self, dim, padding_type, norm_layer, activation=nn.ReLU(True), use_dropout=False, conv_kind='default',
|
||||
dilation=1, in_dim=None, groups=1, second_dilation=None):
|
||||
super(ResnetBlock5x5, self).__init__()
|
||||
self.in_dim = in_dim
|
||||
self.dim = dim
|
||||
if second_dilation is None:
|
||||
second_dilation = dilation
|
||||
self.conv_block = self.build_conv_block(dim, padding_type, norm_layer, activation, use_dropout,
|
||||
conv_kind=conv_kind, dilation=dilation, in_dim=in_dim, groups=groups,
|
||||
second_dilation=second_dilation)
|
||||
|
||||
if self.in_dim is not None:
|
||||
self.input_conv = nn.Conv2d(in_dim, dim, 1)
|
||||
|
||||
self.out_channnels = dim
|
||||
|
||||
def build_conv_block(self, dim, padding_type, norm_layer, activation, use_dropout, conv_kind='default',
|
||||
dilation=1, in_dim=None, groups=1, second_dilation=1):
|
||||
conv_layer = get_conv_block_ctor(conv_kind)
|
||||
|
||||
conv_block = []
|
||||
p = 0
|
||||
if padding_type == 'reflect':
|
||||
conv_block += [nn.ReflectionPad2d(dilation * 2)]
|
||||
elif padding_type == 'replicate':
|
||||
conv_block += [nn.ReplicationPad2d(dilation * 2)]
|
||||
elif padding_type == 'zero':
|
||||
p = dilation * 2
|
||||
else:
|
||||
raise NotImplementedError('padding [%s] is not implemented' % padding_type)
|
||||
|
||||
if in_dim is None:
|
||||
in_dim = dim
|
||||
|
||||
conv_block += [conv_layer(in_dim, dim, kernel_size=5, padding=p, dilation=dilation),
|
||||
norm_layer(dim),
|
||||
activation]
|
||||
if use_dropout:
|
||||
conv_block += [nn.Dropout(0.5)]
|
||||
|
||||
p = 0
|
||||
if padding_type == 'reflect':
|
||||
conv_block += [nn.ReflectionPad2d(second_dilation * 2)]
|
||||
elif padding_type == 'replicate':
|
||||
conv_block += [nn.ReplicationPad2d(second_dilation * 2)]
|
||||
elif padding_type == 'zero':
|
||||
p = second_dilation * 2
|
||||
else:
|
||||
raise NotImplementedError('padding [%s] is not implemented' % padding_type)
|
||||
conv_block += [conv_layer(dim, dim, kernel_size=5, padding=p, dilation=second_dilation, groups=groups),
|
||||
norm_layer(dim)]
|
||||
|
||||
return nn.Sequential(*conv_block)
|
||||
|
||||
def forward(self, x):
|
||||
x_before = x
|
||||
if self.in_dim is not None:
|
||||
x = self.input_conv(x)
|
||||
out = x + self.conv_block(x_before)
|
||||
return out
|
||||
|
||||
|
||||
class MultidilatedResnetBlock(nn.Module):
|
||||
def __init__(self, dim, padding_type, conv_layer, norm_layer, activation=nn.ReLU(True), use_dropout=False):
|
||||
super().__init__()
|
||||
self.conv_block = self.build_conv_block(dim, padding_type, conv_layer, norm_layer, activation, use_dropout)
|
||||
|
||||
def build_conv_block(self, dim, padding_type, conv_layer, norm_layer, activation, use_dropout, dilation=1):
|
||||
conv_block = []
|
||||
conv_block += [conv_layer(dim, dim, kernel_size=3, padding_mode=padding_type),
|
||||
norm_layer(dim),
|
||||
activation]
|
||||
if use_dropout:
|
||||
conv_block += [nn.Dropout(0.5)]
|
||||
|
||||
conv_block += [conv_layer(dim, dim, kernel_size=3, padding_mode=padding_type),
|
||||
norm_layer(dim)]
|
||||
|
||||
return nn.Sequential(*conv_block)
|
||||
|
||||
def forward(self, x):
|
||||
out = x + self.conv_block(x)
|
||||
return out
|
||||
|
||||
|
||||
class MultiDilatedGlobalGenerator(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, ngf=64, n_downsampling=3,
|
||||
n_blocks=3, norm_layer=nn.BatchNorm2d,
|
||||
padding_type='reflect', conv_kind='default',
|
||||
deconv_kind='convtranspose', activation=nn.ReLU(True),
|
||||
up_norm_layer=nn.BatchNorm2d, affine=None, up_activation=nn.ReLU(True),
|
||||
add_out_act=True, max_features=1024, multidilation_kwargs={},
|
||||
ffc_positions=None, ffc_kwargs={}):
|
||||
assert (n_blocks >= 0)
|
||||
super().__init__()
|
||||
|
||||
conv_layer = get_conv_block_ctor(conv_kind)
|
||||
resnet_conv_layer = functools.partial(get_conv_block_ctor('multidilated'), **multidilation_kwargs)
|
||||
norm_layer = get_norm_layer(norm_layer)
|
||||
if affine is not None:
|
||||
norm_layer = partial(norm_layer, affine=affine)
|
||||
up_norm_layer = get_norm_layer(up_norm_layer)
|
||||
if affine is not None:
|
||||
up_norm_layer = partial(up_norm_layer, affine=affine)
|
||||
|
||||
model = [nn.ReflectionPad2d(3),
|
||||
conv_layer(input_nc, ngf, kernel_size=7, padding=0),
|
||||
norm_layer(ngf),
|
||||
activation]
|
||||
|
||||
identity = Identity()
|
||||
### downsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** i
|
||||
|
||||
model += [conv_layer(min(max_features, ngf * mult),
|
||||
min(max_features, ngf * mult * 2),
|
||||
kernel_size=3, stride=2, padding=1),
|
||||
norm_layer(min(max_features, ngf * mult * 2)),
|
||||
activation]
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
feats_num_bottleneck = min(max_features, ngf * mult)
|
||||
|
||||
### resnet blocks
|
||||
for i in range(n_blocks):
|
||||
if ffc_positions is not None and i in ffc_positions:
|
||||
model += [FFCResnetBlock(feats_num_bottleneck, padding_type, norm_layer, activation_layer=nn.ReLU,
|
||||
inline=True, **ffc_kwargs)]
|
||||
model += [MultidilatedResnetBlock(feats_num_bottleneck, padding_type=padding_type,
|
||||
conv_layer=resnet_conv_layer, activation=activation,
|
||||
norm_layer=norm_layer)]
|
||||
|
||||
### upsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** (n_downsampling - i)
|
||||
model += deconv_factory(deconv_kind, ngf, mult, up_norm_layer, up_activation, max_features)
|
||||
model += [nn.ReflectionPad2d(3),
|
||||
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
|
||||
if add_out_act:
|
||||
model.append(get_activation('tanh' if add_out_act is True else add_out_act))
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def forward(self, input):
|
||||
return self.model(input)
|
||||
|
||||
class ConfigGlobalGenerator(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, ngf=64, n_downsampling=3,
|
||||
n_blocks=3, norm_layer=nn.BatchNorm2d,
|
||||
padding_type='reflect', conv_kind='default',
|
||||
deconv_kind='convtranspose', activation=nn.ReLU(True),
|
||||
up_norm_layer=nn.BatchNorm2d, affine=None, up_activation=nn.ReLU(True),
|
||||
add_out_act=True, max_features=1024,
|
||||
manual_block_spec=[],
|
||||
resnet_block_kind='multidilatedresnetblock',
|
||||
resnet_conv_kind='multidilated',
|
||||
resnet_dilation=1,
|
||||
multidilation_kwargs={}):
|
||||
assert (n_blocks >= 0)
|
||||
super().__init__()
|
||||
|
||||
conv_layer = get_conv_block_ctor(conv_kind)
|
||||
resnet_conv_layer = functools.partial(get_conv_block_ctor(resnet_conv_kind), **multidilation_kwargs)
|
||||
norm_layer = get_norm_layer(norm_layer)
|
||||
if affine is not None:
|
||||
norm_layer = partial(norm_layer, affine=affine)
|
||||
up_norm_layer = get_norm_layer(up_norm_layer)
|
||||
if affine is not None:
|
||||
up_norm_layer = partial(up_norm_layer, affine=affine)
|
||||
|
||||
model = [nn.ReflectionPad2d(3),
|
||||
conv_layer(input_nc, ngf, kernel_size=7, padding=0),
|
||||
norm_layer(ngf),
|
||||
activation]
|
||||
|
||||
identity = Identity()
|
||||
|
||||
### downsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** i
|
||||
model += [conv_layer(min(max_features, ngf * mult),
|
||||
min(max_features, ngf * mult * 2),
|
||||
kernel_size=3, stride=2, padding=1),
|
||||
norm_layer(min(max_features, ngf * mult * 2)),
|
||||
activation]
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
feats_num_bottleneck = min(max_features, ngf * mult)
|
||||
|
||||
if len(manual_block_spec) == 0:
|
||||
manual_block_spec = [
|
||||
DotDict(lambda : None, {
|
||||
'n_blocks': n_blocks,
|
||||
'use_default': True})
|
||||
]
|
||||
|
||||
### resnet blocks
|
||||
for block_spec in manual_block_spec:
|
||||
def make_and_add_blocks(model, block_spec):
|
||||
block_spec = DotDict(lambda : None, block_spec)
|
||||
if not block_spec.use_default:
|
||||
resnet_conv_layer = functools.partial(get_conv_block_ctor(block_spec.resnet_conv_kind), **block_spec.multidilation_kwargs)
|
||||
resnet_conv_kind = block_spec.resnet_conv_kind
|
||||
resnet_block_kind = block_spec.resnet_block_kind
|
||||
if block_spec.resnet_dilation is not None:
|
||||
resnet_dilation = block_spec.resnet_dilation
|
||||
for i in range(block_spec.n_blocks):
|
||||
if resnet_block_kind == "multidilatedresnetblock":
|
||||
model += [MultidilatedResnetBlock(feats_num_bottleneck, padding_type=padding_type,
|
||||
conv_layer=resnet_conv_layer, activation=activation,
|
||||
norm_layer=norm_layer)]
|
||||
if resnet_block_kind == "resnetblock":
|
||||
model += [ResnetBlock(ngf * mult, padding_type=padding_type, activation=activation, norm_layer=norm_layer,
|
||||
conv_kind=resnet_conv_kind)]
|
||||
if resnet_block_kind == "resnetblock5x5":
|
||||
model += [ResnetBlock5x5(ngf * mult, padding_type=padding_type, activation=activation, norm_layer=norm_layer,
|
||||
conv_kind=resnet_conv_kind)]
|
||||
if resnet_block_kind == "resnetblockdwdil":
|
||||
model += [ResnetBlock(ngf * mult, padding_type=padding_type, activation=activation, norm_layer=norm_layer,
|
||||
conv_kind=resnet_conv_kind, dilation=resnet_dilation, second_dilation=resnet_dilation)]
|
||||
make_and_add_blocks(model, block_spec)
|
||||
|
||||
### upsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** (n_downsampling - i)
|
||||
model += deconv_factory(deconv_kind, ngf, mult, up_norm_layer, up_activation, max_features)
|
||||
model += [nn.ReflectionPad2d(3),
|
||||
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
|
||||
if add_out_act:
|
||||
model.append(get_activation('tanh' if add_out_act is True else add_out_act))
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def forward(self, input):
|
||||
return self.model(input)
|
||||
|
||||
|
||||
def make_dil_blocks(dilated_blocks_n, dilation_block_kind, dilated_block_kwargs):
|
||||
blocks = []
|
||||
for i in range(dilated_blocks_n):
|
||||
if dilation_block_kind == 'simple':
|
||||
blocks.append(ResnetBlock(**dilated_block_kwargs, dilation=2 ** (i + 1)))
|
||||
elif dilation_block_kind == 'multi':
|
||||
blocks.append(MultidilatedResnetBlock(**dilated_block_kwargs))
|
||||
else:
|
||||
raise ValueError(f'dilation_block_kind could not be "{dilation_block_kind}"')
|
||||
return blocks
|
||||
|
||||
|
||||
class GlobalGenerator(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, ngf=64, n_downsampling=3, n_blocks=9, norm_layer=nn.BatchNorm2d,
|
||||
padding_type='reflect', conv_kind='default', activation=nn.ReLU(True),
|
||||
up_norm_layer=nn.BatchNorm2d, affine=None,
|
||||
up_activation=nn.ReLU(True), dilated_blocks_n=0, dilated_blocks_n_start=0,
|
||||
dilated_blocks_n_middle=0,
|
||||
add_out_act=True,
|
||||
max_features=1024, is_resblock_depthwise=False,
|
||||
ffc_positions=None, ffc_kwargs={}, dilation=1, second_dilation=None,
|
||||
dilation_block_kind='simple', multidilation_kwargs={}):
|
||||
assert (n_blocks >= 0)
|
||||
super().__init__()
|
||||
|
||||
conv_layer = get_conv_block_ctor(conv_kind)
|
||||
norm_layer = get_norm_layer(norm_layer)
|
||||
if affine is not None:
|
||||
norm_layer = partial(norm_layer, affine=affine)
|
||||
up_norm_layer = get_norm_layer(up_norm_layer)
|
||||
if affine is not None:
|
||||
up_norm_layer = partial(up_norm_layer, affine=affine)
|
||||
|
||||
if ffc_positions is not None:
|
||||
ffc_positions = collections.Counter(ffc_positions)
|
||||
|
||||
model = [nn.ReflectionPad2d(3),
|
||||
conv_layer(input_nc, ngf, kernel_size=7, padding=0),
|
||||
norm_layer(ngf),
|
||||
activation]
|
||||
|
||||
identity = Identity()
|
||||
### downsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** i
|
||||
|
||||
model += [conv_layer(min(max_features, ngf * mult),
|
||||
min(max_features, ngf * mult * 2),
|
||||
kernel_size=3, stride=2, padding=1),
|
||||
norm_layer(min(max_features, ngf * mult * 2)),
|
||||
activation]
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
feats_num_bottleneck = min(max_features, ngf * mult)
|
||||
|
||||
dilated_block_kwargs = dict(dim=feats_num_bottleneck, padding_type=padding_type,
|
||||
activation=activation, norm_layer=norm_layer)
|
||||
if dilation_block_kind == 'simple':
|
||||
dilated_block_kwargs['conv_kind'] = conv_kind
|
||||
elif dilation_block_kind == 'multi':
|
||||
dilated_block_kwargs['conv_layer'] = functools.partial(
|
||||
get_conv_block_ctor('multidilated'), **multidilation_kwargs)
|
||||
|
||||
# dilated blocks at the start of the bottleneck sausage
|
||||
if dilated_blocks_n_start is not None and dilated_blocks_n_start > 0:
|
||||
model += make_dil_blocks(dilated_blocks_n_start, dilation_block_kind, dilated_block_kwargs)
|
||||
|
||||
# resnet blocks
|
||||
for i in range(n_blocks):
|
||||
# dilated blocks at the middle of the bottleneck sausage
|
||||
if i == n_blocks // 2 and dilated_blocks_n_middle is not None and dilated_blocks_n_middle > 0:
|
||||
model += make_dil_blocks(dilated_blocks_n_middle, dilation_block_kind, dilated_block_kwargs)
|
||||
|
||||
if ffc_positions is not None and i in ffc_positions:
|
||||
for _ in range(ffc_positions[i]): # same position can occur more than once
|
||||
model += [FFCResnetBlock(feats_num_bottleneck, padding_type, norm_layer, activation_layer=nn.ReLU,
|
||||
inline=True, **ffc_kwargs)]
|
||||
|
||||
if is_resblock_depthwise:
|
||||
resblock_groups = feats_num_bottleneck
|
||||
else:
|
||||
resblock_groups = 1
|
||||
|
||||
model += [ResnetBlock(feats_num_bottleneck, padding_type=padding_type, activation=activation,
|
||||
norm_layer=norm_layer, conv_kind=conv_kind, groups=resblock_groups,
|
||||
dilation=dilation, second_dilation=second_dilation)]
|
||||
|
||||
|
||||
# dilated blocks at the end of the bottleneck sausage
|
||||
if dilated_blocks_n is not None and dilated_blocks_n > 0:
|
||||
model += make_dil_blocks(dilated_blocks_n, dilation_block_kind, dilated_block_kwargs)
|
||||
|
||||
# upsample
|
||||
for i in range(n_downsampling):
|
||||
mult = 2 ** (n_downsampling - i)
|
||||
model += [nn.ConvTranspose2d(min(max_features, ngf * mult),
|
||||
min(max_features, int(ngf * mult / 2)),
|
||||
kernel_size=3, stride=2, padding=1, output_padding=1),
|
||||
up_norm_layer(min(max_features, int(ngf * mult / 2))),
|
||||
up_activation]
|
||||
model += [nn.ReflectionPad2d(3),
|
||||
nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]
|
||||
if add_out_act:
|
||||
model.append(get_activation('tanh' if add_out_act is True else add_out_act))
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def forward(self, input):
|
||||
return self.model(input)
|
||||
|
||||
|
||||
class GlobalGeneratorGated(GlobalGenerator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
real_kwargs=dict(
|
||||
conv_kind='gated_bn_relu',
|
||||
activation=nn.Identity(),
|
||||
norm_layer=nn.Identity
|
||||
)
|
||||
real_kwargs.update(kwargs)
|
||||
super().__init__(*args, **real_kwargs)
|
||||
|
||||
|
||||
class GlobalGeneratorFromSuperChannels(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, n_downsampling, n_blocks, super_channels, norm_layer="bn", padding_type='reflect', add_out_act=True):
|
||||
super().__init__()
|
||||
self.n_downsampling = n_downsampling
|
||||
norm_layer = get_norm_layer(norm_layer)
|
||||
if type(norm_layer) == functools.partial:
|
||||
use_bias = (norm_layer.func == nn.InstanceNorm2d)
|
||||
else:
|
||||
use_bias = (norm_layer == nn.InstanceNorm2d)
|
||||
|
||||
channels = self.convert_super_channels(super_channels)
|
||||
self.channels = channels
|
||||
|
||||
model = [nn.ReflectionPad2d(3),
|
||||
nn.Conv2d(input_nc, channels[0], kernel_size=7, padding=0, bias=use_bias),
|
||||
norm_layer(channels[0]),
|
||||
nn.ReLU(True)]
|
||||
|
||||
for i in range(n_downsampling): # add downsampling layers
|
||||
mult = 2 ** i
|
||||
model += [nn.Conv2d(channels[0+i], channels[1+i], kernel_size=3, stride=2, padding=1, bias=use_bias),
|
||||
norm_layer(channels[1+i]),
|
||||
nn.ReLU(True)]
|
||||
|
||||
mult = 2 ** n_downsampling
|
||||
|
||||
n_blocks1 = n_blocks // 3
|
||||
n_blocks2 = n_blocks1
|
||||
n_blocks3 = n_blocks - n_blocks1 - n_blocks2
|
||||
|
||||
for i in range(n_blocks1):
|
||||
c = n_downsampling
|
||||
dim = channels[c]
|
||||
model += [ResnetBlock(dim, padding_type=padding_type, norm_layer=norm_layer)]
|
||||
|
||||
for i in range(n_blocks2):
|
||||
c = n_downsampling+1
|
||||
dim = channels[c]
|
||||
kwargs = {}
|
||||
if i == 0:
|
||||
kwargs = {"in_dim": channels[c-1]}
|
||||
model += [ResnetBlock(dim, padding_type=padding_type, norm_layer=norm_layer, **kwargs)]
|
||||
|
||||
for i in range(n_blocks3):
|
||||
c = n_downsampling+2
|
||||
dim = channels[c]
|
||||
kwargs = {}
|
||||
if i == 0:
|
||||
kwargs = {"in_dim": channels[c-1]}
|
||||
model += [ResnetBlock(dim, padding_type=padding_type, norm_layer=norm_layer, **kwargs)]
|
||||
|
||||
for i in range(n_downsampling): # add upsampling layers
|
||||
mult = 2 ** (n_downsampling - i)
|
||||
model += [nn.ConvTranspose2d(channels[n_downsampling+3+i],
|
||||
channels[n_downsampling+3+i+1],
|
||||
kernel_size=3, stride=2,
|
||||
padding=1, output_padding=1,
|
||||
bias=use_bias),
|
||||
norm_layer(channels[n_downsampling+3+i+1]),
|
||||
nn.ReLU(True)]
|
||||
model += [nn.ReflectionPad2d(3)]
|
||||
model += [nn.Conv2d(channels[2*n_downsampling+3], output_nc, kernel_size=7, padding=0)]
|
||||
|
||||
if add_out_act:
|
||||
model.append(get_activation('tanh' if add_out_act is True else add_out_act))
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def convert_super_channels(self, super_channels):
|
||||
n_downsampling = self.n_downsampling
|
||||
result = []
|
||||
cnt = 0
|
||||
|
||||
if n_downsampling == 2:
|
||||
N1 = 10
|
||||
elif n_downsampling == 3:
|
||||
N1 = 13
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
for i in range(0, N1):
|
||||
if i in [1,4,7,10]:
|
||||
channel = super_channels[cnt] * (2 ** cnt)
|
||||
config = {'channel': channel}
|
||||
result.append(channel)
|
||||
logging.info(f"Downsample channels {result[-1]}")
|
||||
cnt += 1
|
||||
|
||||
for i in range(3):
|
||||
for counter, j in enumerate(range(N1 + i * 3, N1 + 3 + i * 3)):
|
||||
if len(super_channels) == 6:
|
||||
channel = super_channels[3] * 4
|
||||
else:
|
||||
channel = super_channels[i + 3] * 4
|
||||
config = {'channel': channel}
|
||||
if counter == 0:
|
||||
result.append(channel)
|
||||
logging.info(f"Bottleneck channels {result[-1]}")
|
||||
cnt = 2
|
||||
|
||||
for i in range(N1+9, N1+21):
|
||||
if i in [22, 25,28]:
|
||||
cnt -= 1
|
||||
if len(super_channels) == 6:
|
||||
channel = super_channels[5 - cnt] * (2 ** cnt)
|
||||
else:
|
||||
channel = super_channels[7 - cnt] * (2 ** cnt)
|
||||
result.append(int(channel))
|
||||
logging.info(f"Upsample channels {result[-1]}")
|
||||
return result
|
||||
|
||||
def forward(self, input):
|
||||
return self.model(input)
|
||||
|
||||
|
||||
# Defines the PatchGAN discriminator with the specified arguments.
|
||||
class NLayerDiscriminator(BaseDiscriminator):
|
||||
def __init__(self, input_nc, ndf=64, n_layers=3, norm_layer=nn.BatchNorm2d,):
|
||||
super().__init__()
|
||||
self.n_layers = n_layers
|
||||
|
||||
kw = 4
|
||||
padw = int(np.ceil((kw-1.0)/2))
|
||||
sequence = [[nn.Conv2d(input_nc, ndf, kernel_size=kw, stride=2, padding=padw),
|
||||
nn.LeakyReLU(0.2, True)]]
|
||||
|
||||
nf = ndf
|
||||
for n in range(1, n_layers):
|
||||
nf_prev = nf
|
||||
nf = min(nf * 2, 512)
|
||||
|
||||
cur_model = []
|
||||
cur_model += [
|
||||
nn.Conv2d(nf_prev, nf, kernel_size=kw, stride=2, padding=padw),
|
||||
norm_layer(nf),
|
||||
nn.LeakyReLU(0.2, True)
|
||||
]
|
||||
sequence.append(cur_model)
|
||||
|
||||
nf_prev = nf
|
||||
nf = min(nf * 2, 512)
|
||||
|
||||
cur_model = []
|
||||
cur_model += [
|
||||
nn.Conv2d(nf_prev, nf, kernel_size=kw, stride=1, padding=padw),
|
||||
norm_layer(nf),
|
||||
nn.LeakyReLU(0.2, True)
|
||||
]
|
||||
sequence.append(cur_model)
|
||||
|
||||
sequence += [[nn.Conv2d(nf, 1, kernel_size=kw, stride=1, padding=padw)]]
|
||||
|
||||
for n in range(len(sequence)):
|
||||
setattr(self, 'model'+str(n), nn.Sequential(*sequence[n]))
|
||||
|
||||
def get_all_activations(self, x):
|
||||
res = [x]
|
||||
for n in range(self.n_layers + 2):
|
||||
model = getattr(self, 'model' + str(n))
|
||||
res.append(model(res[-1]))
|
||||
return res[1:]
|
||||
|
||||
def forward(self, x):
|
||||
act = self.get_all_activations(x)
|
||||
return act[-1], act[:-1]
|
||||
|
||||
|
||||
class MultidilatedNLayerDiscriminator(BaseDiscriminator):
|
||||
def __init__(self, input_nc, ndf=64, n_layers=3, norm_layer=nn.BatchNorm2d, multidilation_kwargs={}):
|
||||
super().__init__()
|
||||
self.n_layers = n_layers
|
||||
|
||||
kw = 4
|
||||
padw = int(np.ceil((kw-1.0)/2))
|
||||
sequence = [[nn.Conv2d(input_nc, ndf, kernel_size=kw, stride=2, padding=padw),
|
||||
nn.LeakyReLU(0.2, True)]]
|
||||
|
||||
nf = ndf
|
||||
for n in range(1, n_layers):
|
||||
nf_prev = nf
|
||||
nf = min(nf * 2, 512)
|
||||
|
||||
cur_model = []
|
||||
cur_model += [
|
||||
MultidilatedConv(nf_prev, nf, kernel_size=kw, stride=2, padding=[2, 3], **multidilation_kwargs),
|
||||
norm_layer(nf),
|
||||
nn.LeakyReLU(0.2, True)
|
||||
]
|
||||
sequence.append(cur_model)
|
||||
|
||||
nf_prev = nf
|
||||
nf = min(nf * 2, 512)
|
||||
|
||||
cur_model = []
|
||||
cur_model += [
|
||||
nn.Conv2d(nf_prev, nf, kernel_size=kw, stride=1, padding=padw),
|
||||
norm_layer(nf),
|
||||
nn.LeakyReLU(0.2, True)
|
||||
]
|
||||
sequence.append(cur_model)
|
||||
|
||||
sequence += [[nn.Conv2d(nf, 1, kernel_size=kw, stride=1, padding=padw)]]
|
||||
|
||||
for n in range(len(sequence)):
|
||||
setattr(self, 'model'+str(n), nn.Sequential(*sequence[n]))
|
||||
|
||||
def get_all_activations(self, x):
|
||||
res = [x]
|
||||
for n in range(self.n_layers + 2):
|
||||
model = getattr(self, 'model' + str(n))
|
||||
res.append(model(res[-1]))
|
||||
return res[1:]
|
||||
|
||||
def forward(self, x):
|
||||
act = self.get_all_activations(x)
|
||||
return act[-1], act[:-1]
|
||||
|
||||
|
||||
class NLayerDiscriminatorAsGen(NLayerDiscriminator):
|
||||
def forward(self, x):
|
||||
return super().forward(x)[0]
|
||||
@@ -0,0 +1,49 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from kornia.geometry.transform import rotate
|
||||
|
||||
|
||||
class LearnableSpatialTransformWrapper(nn.Module):
|
||||
def __init__(self, impl, pad_coef=0.5, angle_init_range=80, train_angle=True):
|
||||
super().__init__()
|
||||
self.impl = impl
|
||||
self.angle = torch.rand(1) * angle_init_range
|
||||
if train_angle:
|
||||
self.angle = nn.Parameter(self.angle, requires_grad=True)
|
||||
self.pad_coef = pad_coef
|
||||
|
||||
def forward(self, x):
|
||||
if torch.is_tensor(x):
|
||||
return self.inverse_transform(self.impl(self.transform(x)), x)
|
||||
elif isinstance(x, tuple):
|
||||
x_trans = tuple(self.transform(elem) for elem in x)
|
||||
y_trans = self.impl(x_trans)
|
||||
return tuple(self.inverse_transform(elem, orig_x) for elem, orig_x in zip(y_trans, x))
|
||||
else:
|
||||
raise ValueError(f'Unexpected input type {type(x)}')
|
||||
|
||||
def transform(self, x):
|
||||
height, width = x.shape[2:]
|
||||
pad_h, pad_w = int(height * self.pad_coef), int(width * self.pad_coef)
|
||||
x_padded = F.pad(x, [pad_w, pad_w, pad_h, pad_h], mode='reflect')
|
||||
x_padded_rotated = rotate(x_padded, angle=self.angle.to(x_padded))
|
||||
return x_padded_rotated
|
||||
|
||||
def inverse_transform(self, y_padded_rotated, orig_x):
|
||||
height, width = orig_x.shape[2:]
|
||||
pad_h, pad_w = int(height * self.pad_coef), int(width * self.pad_coef)
|
||||
|
||||
y_padded = rotate(y_padded_rotated, angle=-self.angle.to(y_padded_rotated))
|
||||
y_height, y_width = y_padded.shape[2:]
|
||||
y = y_padded[:, :, pad_h : y_height - pad_h, pad_w : y_width - pad_w]
|
||||
return y
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
layer = LearnableSpatialTransformWrapper(nn.Identity())
|
||||
x = torch.arange(2* 3 * 15 * 15).view(2, 3, 15, 15).float()
|
||||
y = layer(x)
|
||||
assert x.shape == y.shape
|
||||
assert torch.allclose(x[:, :, 1:, 1:][:, :, :-1, :-1], y[:, :, 1:, 1:][:, :, :-1, :-1])
|
||||
print('all ok')
|
||||
@@ -0,0 +1,20 @@
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class SELayer(nn.Module):
|
||||
def __init__(self, channel, reduction=16):
|
||||
super(SELayer, self).__init__()
|
||||
self.avg_pool = nn.AdaptiveAvgPool2d(1)
|
||||
self.fc = nn.Sequential(
|
||||
nn.Linear(channel, channel // reduction, bias=False),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Linear(channel // reduction, channel, bias=False),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
b, c, _, _ = x.size()
|
||||
y = self.avg_pool(x).view(b, c)
|
||||
y = self.fc(y).view(b, c, 1, 1)
|
||||
res = x * y.expand_as(x)
|
||||
return res
|
||||
@@ -0,0 +1,30 @@
|
||||
import logging
|
||||
import torch
|
||||
from saicinpainting.training.trainers.default import DefaultInpaintingTrainingModule
|
||||
|
||||
|
||||
def get_training_model_class(kind):
|
||||
if kind == 'default':
|
||||
return DefaultInpaintingTrainingModule
|
||||
|
||||
raise ValueError(f'Unknown trainer module {kind}')
|
||||
|
||||
|
||||
def make_training_model(config):
|
||||
kind = config.training_model.kind
|
||||
kwargs = dict(config.training_model)
|
||||
kwargs.pop('kind')
|
||||
kwargs['use_ddp'] = config.trainer.kwargs.get('accelerator', None) == 'ddp'
|
||||
|
||||
logging.info(f'Make training model {kind}')
|
||||
|
||||
cls = get_training_model_class(kind)
|
||||
return cls(config, **kwargs)
|
||||
|
||||
|
||||
def load_checkpoint(train_config, path, map_location='cuda', strict=True):
|
||||
model: torch.nn.Module = make_training_model(train_config)
|
||||
state = torch.load(path, map_location=map_location)
|
||||
model.load_state_dict(state['state_dict'], strict=strict)
|
||||
model.on_load_checkpoint(state)
|
||||
return model
|
||||
@@ -0,0 +1,291 @@
|
||||
import copy
|
||||
import logging
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import pandas as pd
|
||||
import pytorch_lightning as ptl
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.data import DistributedSampler
|
||||
|
||||
from saicinpainting.evaluation import make_evaluator
|
||||
from saicinpainting.training.data.datasets import make_default_train_dataloader, make_default_val_dataloader
|
||||
from saicinpainting.training.losses.adversarial import make_discrim_loss
|
||||
from saicinpainting.training.losses.perceptual import PerceptualLoss, ResNetPL
|
||||
from saicinpainting.training.modules import make_generator, make_discriminator
|
||||
from saicinpainting.training.visualizers import make_visualizer
|
||||
from saicinpainting.utils import add_prefix_to_keys, average_dicts, set_requires_grad, flatten_dict, \
|
||||
get_has_ddp_rank
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_optimizer(parameters, kind='adamw', **kwargs):
|
||||
if kind == 'adam':
|
||||
optimizer_class = torch.optim.Adam
|
||||
elif kind == 'adamw':
|
||||
optimizer_class = torch.optim.AdamW
|
||||
else:
|
||||
raise ValueError(f'Unknown optimizer kind {kind}')
|
||||
return optimizer_class(parameters, **kwargs)
|
||||
|
||||
|
||||
def update_running_average(result: nn.Module, new_iterate_model: nn.Module, decay=0.999):
|
||||
with torch.no_grad():
|
||||
res_params = dict(result.named_parameters())
|
||||
new_params = dict(new_iterate_model.named_parameters())
|
||||
|
||||
for k in res_params.keys():
|
||||
res_params[k].data.mul_(decay).add_(new_params[k].data, alpha=1 - decay)
|
||||
|
||||
|
||||
def make_multiscale_noise(base_tensor, scales=6, scale_mode='bilinear'):
|
||||
batch_size, _, height, width = base_tensor.shape
|
||||
cur_height, cur_width = height, width
|
||||
result = []
|
||||
align_corners = False if scale_mode in ('bilinear', 'bicubic') else None
|
||||
for _ in range(scales):
|
||||
cur_sample = torch.randn(batch_size, 1, cur_height, cur_width, device=base_tensor.device)
|
||||
cur_sample_scaled = F.interpolate(cur_sample, size=(height, width), mode=scale_mode, align_corners=align_corners)
|
||||
result.append(cur_sample_scaled)
|
||||
cur_height //= 2
|
||||
cur_width //= 2
|
||||
return torch.cat(result, dim=1)
|
||||
|
||||
|
||||
class BaseInpaintingTrainingModule(ptl.LightningModule):
|
||||
def __init__(self, config, use_ddp, *args, predict_only=False, visualize_each_iters=100,
|
||||
average_generator=False, generator_avg_beta=0.999, average_generator_start_step=30000,
|
||||
average_generator_period=10, store_discr_outputs_for_vis=False,
|
||||
**kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
LOGGER.info('BaseInpaintingTrainingModule init called')
|
||||
|
||||
self.config = config
|
||||
|
||||
self.generator = make_generator(config, **self.config.generator)
|
||||
self.use_ddp = use_ddp
|
||||
|
||||
if not get_has_ddp_rank():
|
||||
LOGGER.info(f'Generator\n{self.generator}')
|
||||
|
||||
if not predict_only:
|
||||
self.save_hyperparameters(self.config)
|
||||
self.discriminator = make_discriminator(**self.config.discriminator)
|
||||
self.adversarial_loss = make_discrim_loss(**self.config.losses.adversarial)
|
||||
self.visualizer = make_visualizer(**self.config.visualizer)
|
||||
self.val_evaluator = make_evaluator(**self.config.evaluator)
|
||||
self.test_evaluator = make_evaluator(**self.config.evaluator)
|
||||
|
||||
if not get_has_ddp_rank():
|
||||
LOGGER.info(f'Discriminator\n{self.discriminator}')
|
||||
|
||||
extra_val = self.config.data.get('extra_val', ())
|
||||
if extra_val:
|
||||
self.extra_val_titles = list(extra_val)
|
||||
self.extra_evaluators = nn.ModuleDict({k: make_evaluator(**self.config.evaluator)
|
||||
for k in extra_val})
|
||||
else:
|
||||
self.extra_evaluators = {}
|
||||
|
||||
self.average_generator = average_generator
|
||||
self.generator_avg_beta = generator_avg_beta
|
||||
self.average_generator_start_step = average_generator_start_step
|
||||
self.average_generator_period = average_generator_period
|
||||
self.generator_average = None
|
||||
self.last_generator_averaging_step = -1
|
||||
self.store_discr_outputs_for_vis = store_discr_outputs_for_vis
|
||||
|
||||
if self.config.losses.get("l1", {"weight_known": 0})['weight_known'] > 0:
|
||||
self.loss_l1 = nn.L1Loss(reduction='none')
|
||||
|
||||
if self.config.losses.get("mse", {"weight": 0})['weight'] > 0:
|
||||
self.loss_mse = nn.MSELoss(reduction='none')
|
||||
|
||||
if self.config.losses.perceptual.weight > 0:
|
||||
self.loss_pl = PerceptualLoss()
|
||||
|
||||
if self.config.losses.get("resnet_pl", {"weight": 0})['weight'] > 0:
|
||||
self.loss_resnet_pl = ResNetPL(**self.config.losses.resnet_pl)
|
||||
else:
|
||||
self.loss_resnet_pl = None
|
||||
|
||||
self.visualize_each_iters = visualize_each_iters
|
||||
LOGGER.info('BaseInpaintingTrainingModule init done')
|
||||
|
||||
def configure_optimizers(self):
|
||||
discriminator_params = list(self.discriminator.parameters())
|
||||
return [
|
||||
dict(optimizer=make_optimizer(self.generator.parameters(), **self.config.optimizers.generator)),
|
||||
dict(optimizer=make_optimizer(discriminator_params, **self.config.optimizers.discriminator)),
|
||||
]
|
||||
|
||||
def train_dataloader(self):
|
||||
kwargs = dict(self.config.data.train)
|
||||
if self.use_ddp:
|
||||
kwargs['ddp_kwargs'] = dict(num_replicas=self.trainer.num_nodes * self.trainer.num_processes,
|
||||
rank=self.trainer.global_rank,
|
||||
shuffle=True)
|
||||
dataloader = make_default_train_dataloader(**self.config.data.train)
|
||||
return dataloader
|
||||
|
||||
def val_dataloader(self):
|
||||
res = [make_default_val_dataloader(**self.config.data.val)]
|
||||
|
||||
if self.config.data.visual_test is not None:
|
||||
res = res + [make_default_val_dataloader(**self.config.data.visual_test)]
|
||||
else:
|
||||
res = res + res
|
||||
|
||||
extra_val = self.config.data.get('extra_val', ())
|
||||
if extra_val:
|
||||
res += [make_default_val_dataloader(**extra_val[k]) for k in self.extra_val_titles]
|
||||
|
||||
return res
|
||||
|
||||
def training_step(self, batch, batch_idx, optimizer_idx=None):
|
||||
self._is_training_step = True
|
||||
return self._do_step(batch, batch_idx, mode='train', optimizer_idx=optimizer_idx)
|
||||
|
||||
def validation_step(self, batch, batch_idx, dataloader_idx):
|
||||
extra_val_key = None
|
||||
if dataloader_idx == 0:
|
||||
mode = 'val'
|
||||
elif dataloader_idx == 1:
|
||||
mode = 'test'
|
||||
else:
|
||||
mode = 'extra_val'
|
||||
extra_val_key = self.extra_val_titles[dataloader_idx - 2]
|
||||
self._is_training_step = False
|
||||
return self._do_step(batch, batch_idx, mode=mode, extra_val_key=extra_val_key)
|
||||
|
||||
def training_step_end(self, batch_parts_outputs):
|
||||
if self.training and self.average_generator \
|
||||
and self.global_step >= self.average_generator_start_step \
|
||||
and self.global_step >= self.last_generator_averaging_step + self.average_generator_period:
|
||||
if self.generator_average is None:
|
||||
self.generator_average = copy.deepcopy(self.generator)
|
||||
else:
|
||||
update_running_average(self.generator_average, self.generator, decay=self.generator_avg_beta)
|
||||
self.last_generator_averaging_step = self.global_step
|
||||
|
||||
full_loss = (batch_parts_outputs['loss'].mean()
|
||||
if torch.is_tensor(batch_parts_outputs['loss']) # loss is not tensor when no discriminator used
|
||||
else torch.tensor(batch_parts_outputs['loss']).float().requires_grad_(True))
|
||||
log_info = {k: v.mean() for k, v in batch_parts_outputs['log_info'].items()}
|
||||
self.log_dict(log_info, on_step=True, on_epoch=False)
|
||||
return full_loss
|
||||
|
||||
def validation_epoch_end(self, outputs):
|
||||
outputs = [step_out for out_group in outputs for step_out in out_group]
|
||||
averaged_logs = average_dicts(step_out['log_info'] for step_out in outputs)
|
||||
self.log_dict({k: v.mean() for k, v in averaged_logs.items()})
|
||||
|
||||
pd.set_option('display.max_columns', 500)
|
||||
pd.set_option('display.width', 1000)
|
||||
|
||||
# standard validation
|
||||
val_evaluator_states = [s['val_evaluator_state'] for s in outputs if 'val_evaluator_state' in s]
|
||||
val_evaluator_res = self.val_evaluator.evaluation_end(states=val_evaluator_states)
|
||||
val_evaluator_res_df = pd.DataFrame(val_evaluator_res).stack(1).unstack(0)
|
||||
val_evaluator_res_df.dropna(axis=1, how='all', inplace=True)
|
||||
LOGGER.info(f'Validation metrics after epoch #{self.current_epoch}, '
|
||||
f'total {self.global_step} iterations:\n{val_evaluator_res_df}')
|
||||
|
||||
for k, v in flatten_dict(val_evaluator_res).items():
|
||||
self.log(f'val_{k}', v)
|
||||
|
||||
# standard visual test
|
||||
test_evaluator_states = [s['test_evaluator_state'] for s in outputs
|
||||
if 'test_evaluator_state' in s]
|
||||
test_evaluator_res = self.test_evaluator.evaluation_end(states=test_evaluator_states)
|
||||
test_evaluator_res_df = pd.DataFrame(test_evaluator_res).stack(1).unstack(0)
|
||||
test_evaluator_res_df.dropna(axis=1, how='all', inplace=True)
|
||||
LOGGER.info(f'Test metrics after epoch #{self.current_epoch}, '
|
||||
f'total {self.global_step} iterations:\n{test_evaluator_res_df}')
|
||||
|
||||
for k, v in flatten_dict(test_evaluator_res).items():
|
||||
self.log(f'test_{k}', v)
|
||||
|
||||
# extra validations
|
||||
if self.extra_evaluators:
|
||||
for cur_eval_title, cur_evaluator in self.extra_evaluators.items():
|
||||
cur_state_key = f'extra_val_{cur_eval_title}_evaluator_state'
|
||||
cur_states = [s[cur_state_key] for s in outputs if cur_state_key in s]
|
||||
cur_evaluator_res = cur_evaluator.evaluation_end(states=cur_states)
|
||||
cur_evaluator_res_df = pd.DataFrame(cur_evaluator_res).stack(1).unstack(0)
|
||||
cur_evaluator_res_df.dropna(axis=1, how='all', inplace=True)
|
||||
LOGGER.info(f'Extra val {cur_eval_title} metrics after epoch #{self.current_epoch}, '
|
||||
f'total {self.global_step} iterations:\n{cur_evaluator_res_df}')
|
||||
for k, v in flatten_dict(cur_evaluator_res).items():
|
||||
self.log(f'extra_val_{cur_eval_title}_{k}', v)
|
||||
|
||||
def _do_step(self, batch, batch_idx, mode='train', optimizer_idx=None, extra_val_key=None):
|
||||
if optimizer_idx == 0: # step for generator
|
||||
set_requires_grad(self.generator, True)
|
||||
set_requires_grad(self.discriminator, False)
|
||||
elif optimizer_idx == 1: # step for discriminator
|
||||
set_requires_grad(self.generator, False)
|
||||
set_requires_grad(self.discriminator, True)
|
||||
|
||||
batch = self(batch)
|
||||
|
||||
total_loss = 0
|
||||
metrics = {}
|
||||
|
||||
if optimizer_idx is None or optimizer_idx == 0: # step for generator
|
||||
total_loss, metrics = self.generator_loss(batch)
|
||||
|
||||
elif optimizer_idx is None or optimizer_idx == 1: # step for discriminator
|
||||
if self.config.losses.adversarial.weight > 0:
|
||||
total_loss, metrics = self.discriminator_loss(batch)
|
||||
|
||||
if self.get_ddp_rank() in (None, 0) and (batch_idx % self.visualize_each_iters == 0 or mode == 'test'):
|
||||
if self.config.losses.adversarial.weight > 0:
|
||||
if self.store_discr_outputs_for_vis:
|
||||
with torch.no_grad():
|
||||
self.store_discr_outputs(batch)
|
||||
vis_suffix = f'_{mode}'
|
||||
if mode == 'extra_val':
|
||||
vis_suffix += f'_{extra_val_key}'
|
||||
self.visualizer(self.current_epoch, batch_idx, batch, suffix=vis_suffix)
|
||||
|
||||
metrics_prefix = f'{mode}_'
|
||||
if mode == 'extra_val':
|
||||
metrics_prefix += f'{extra_val_key}_'
|
||||
result = dict(loss=total_loss, log_info=add_prefix_to_keys(metrics, metrics_prefix))
|
||||
if mode == 'val':
|
||||
result['val_evaluator_state'] = self.val_evaluator.process_batch(batch)
|
||||
elif mode == 'test':
|
||||
result['test_evaluator_state'] = self.test_evaluator.process_batch(batch)
|
||||
elif mode == 'extra_val':
|
||||
result[f'extra_val_{extra_val_key}_evaluator_state'] = self.extra_evaluators[extra_val_key].process_batch(batch)
|
||||
|
||||
return result
|
||||
|
||||
def get_current_generator(self, no_average=False):
|
||||
if not no_average and not self.training and self.average_generator and self.generator_average is not None:
|
||||
return self.generator_average
|
||||
return self.generator
|
||||
|
||||
def forward(self, batch: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
|
||||
"""Pass data through generator and obtain at leas 'predicted_image' and 'inpainted' keys"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def generator_loss(self, batch) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def discriminator_loss(self, batch) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def store_discr_outputs(self, batch):
|
||||
out_size = batch['image'].shape[2:]
|
||||
discr_real_out, _ = self.discriminator(batch['image'])
|
||||
discr_fake_out, _ = self.discriminator(batch['predicted_image'])
|
||||
batch['discr_output_real'] = F.interpolate(discr_real_out, size=out_size, mode='nearest')
|
||||
batch['discr_output_fake'] = F.interpolate(discr_fake_out, size=out_size, mode='nearest')
|
||||
batch['discr_output_diff'] = batch['discr_output_real'] - batch['discr_output_fake']
|
||||
|
||||
def get_ddp_rank(self):
|
||||
return self.trainer.global_rank if (self.trainer.num_nodes * self.trainer.num_processes) > 1 else None
|
||||
@@ -0,0 +1,175 @@
|
||||
import logging
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
from saicinpainting.training.data.datasets import make_constant_area_crop_params
|
||||
from saicinpainting.training.losses.distance_weighting import make_mask_distance_weighter
|
||||
from saicinpainting.training.losses.feature_matching import feature_matching_loss, masked_l1_loss
|
||||
from saicinpainting.training.modules.fake_fakes import FakeFakesGenerator
|
||||
from saicinpainting.training.trainers.base import BaseInpaintingTrainingModule, make_multiscale_noise
|
||||
from saicinpainting.utils import add_prefix_to_keys, get_ramp
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_constant_area_crop_batch(batch, **kwargs):
|
||||
crop_y, crop_x, crop_height, crop_width = make_constant_area_crop_params(img_height=batch['image'].shape[2],
|
||||
img_width=batch['image'].shape[3],
|
||||
**kwargs)
|
||||
batch['image'] = batch['image'][:, :, crop_y : crop_y + crop_height, crop_x : crop_x + crop_width]
|
||||
batch['mask'] = batch['mask'][:, :, crop_y: crop_y + crop_height, crop_x: crop_x + crop_width]
|
||||
return batch
|
||||
|
||||
|
||||
class DefaultInpaintingTrainingModule(BaseInpaintingTrainingModule):
|
||||
def __init__(self, *args, concat_mask=True, rescale_scheduler_kwargs=None, image_to_discriminator='predicted_image',
|
||||
add_noise_kwargs=None, noise_fill_hole=False, const_area_crop_kwargs=None,
|
||||
distance_weighter_kwargs=None, distance_weighted_mask_for_discr=False,
|
||||
fake_fakes_proba=0, fake_fakes_generator_kwargs=None,
|
||||
**kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.concat_mask = concat_mask
|
||||
self.rescale_size_getter = get_ramp(**rescale_scheduler_kwargs) if rescale_scheduler_kwargs is not None else None
|
||||
self.image_to_discriminator = image_to_discriminator
|
||||
self.add_noise_kwargs = add_noise_kwargs
|
||||
self.noise_fill_hole = noise_fill_hole
|
||||
self.const_area_crop_kwargs = const_area_crop_kwargs
|
||||
self.refine_mask_for_losses = make_mask_distance_weighter(**distance_weighter_kwargs) \
|
||||
if distance_weighter_kwargs is not None else None
|
||||
self.distance_weighted_mask_for_discr = distance_weighted_mask_for_discr
|
||||
|
||||
self.fake_fakes_proba = fake_fakes_proba
|
||||
if self.fake_fakes_proba > 1e-3:
|
||||
self.fake_fakes_gen = FakeFakesGenerator(**(fake_fakes_generator_kwargs or {}))
|
||||
|
||||
def forward(self, batch):
|
||||
if self.training and self.rescale_size_getter is not None:
|
||||
cur_size = self.rescale_size_getter(self.global_step)
|
||||
batch['image'] = F.interpolate(batch['image'], size=cur_size, mode='bilinear', align_corners=False)
|
||||
batch['mask'] = F.interpolate(batch['mask'], size=cur_size, mode='nearest')
|
||||
|
||||
if self.training and self.const_area_crop_kwargs is not None:
|
||||
batch = make_constant_area_crop_batch(batch, **self.const_area_crop_kwargs)
|
||||
|
||||
img = batch['image']
|
||||
mask = batch['mask']
|
||||
|
||||
masked_img = img * (1 - mask)
|
||||
|
||||
if self.add_noise_kwargs is not None:
|
||||
noise = make_multiscale_noise(masked_img, **self.add_noise_kwargs)
|
||||
if self.noise_fill_hole:
|
||||
masked_img = masked_img + mask * noise[:, :masked_img.shape[1]]
|
||||
masked_img = torch.cat([masked_img, noise], dim=1)
|
||||
|
||||
if self.concat_mask:
|
||||
masked_img = torch.cat([masked_img, mask], dim=1)
|
||||
|
||||
batch['predicted_image'] = self.generator(masked_img)
|
||||
batch['inpainted'] = mask * batch['predicted_image'] + (1 - mask) * batch['image']
|
||||
|
||||
if self.fake_fakes_proba > 1e-3:
|
||||
if self.training and torch.rand(1).item() < self.fake_fakes_proba:
|
||||
batch['fake_fakes'], batch['fake_fakes_masks'] = self.fake_fakes_gen(img, mask)
|
||||
batch['use_fake_fakes'] = True
|
||||
else:
|
||||
batch['fake_fakes'] = torch.zeros_like(img)
|
||||
batch['fake_fakes_masks'] = torch.zeros_like(mask)
|
||||
batch['use_fake_fakes'] = False
|
||||
|
||||
batch['mask_for_losses'] = self.refine_mask_for_losses(img, batch['predicted_image'], mask) \
|
||||
if self.refine_mask_for_losses is not None and self.training \
|
||||
else mask
|
||||
|
||||
return batch
|
||||
|
||||
def generator_loss(self, batch):
|
||||
img = batch['image']
|
||||
predicted_img = batch[self.image_to_discriminator]
|
||||
original_mask = batch['mask']
|
||||
supervised_mask = batch['mask_for_losses']
|
||||
|
||||
# L1
|
||||
l1_value = masked_l1_loss(predicted_img, img, supervised_mask,
|
||||
self.config.losses.l1.weight_known,
|
||||
self.config.losses.l1.weight_missing)
|
||||
|
||||
total_loss = l1_value
|
||||
metrics = dict(gen_l1=l1_value)
|
||||
|
||||
# vgg-based perceptual loss
|
||||
if self.config.losses.perceptual.weight > 0:
|
||||
pl_value = self.loss_pl(predicted_img, img, mask=supervised_mask).sum() * self.config.losses.perceptual.weight
|
||||
total_loss = total_loss + pl_value
|
||||
metrics['gen_pl'] = pl_value
|
||||
|
||||
# discriminator
|
||||
# adversarial_loss calls backward by itself
|
||||
mask_for_discr = supervised_mask if self.distance_weighted_mask_for_discr else original_mask
|
||||
self.adversarial_loss.pre_generator_step(real_batch=img, fake_batch=predicted_img,
|
||||
generator=self.generator, discriminator=self.discriminator)
|
||||
discr_real_pred, discr_real_features = self.discriminator(img)
|
||||
discr_fake_pred, discr_fake_features = self.discriminator(predicted_img)
|
||||
adv_gen_loss, adv_metrics = self.adversarial_loss.generator_loss(real_batch=img,
|
||||
fake_batch=predicted_img,
|
||||
discr_real_pred=discr_real_pred,
|
||||
discr_fake_pred=discr_fake_pred,
|
||||
mask=mask_for_discr)
|
||||
total_loss = total_loss + adv_gen_loss
|
||||
metrics['gen_adv'] = adv_gen_loss
|
||||
metrics.update(add_prefix_to_keys(adv_metrics, 'adv_'))
|
||||
|
||||
# feature matching
|
||||
if self.config.losses.feature_matching.weight > 0:
|
||||
need_mask_in_fm = OmegaConf.to_container(self.config.losses.feature_matching).get('pass_mask', False)
|
||||
mask_for_fm = supervised_mask if need_mask_in_fm else None
|
||||
fm_value = feature_matching_loss(discr_fake_features, discr_real_features,
|
||||
mask=mask_for_fm) * self.config.losses.feature_matching.weight
|
||||
total_loss = total_loss + fm_value
|
||||
metrics['gen_fm'] = fm_value
|
||||
|
||||
if self.loss_resnet_pl is not None:
|
||||
resnet_pl_value = self.loss_resnet_pl(predicted_img, img)
|
||||
total_loss = total_loss + resnet_pl_value
|
||||
metrics['gen_resnet_pl'] = resnet_pl_value
|
||||
|
||||
return total_loss, metrics
|
||||
|
||||
def discriminator_loss(self, batch):
|
||||
total_loss = 0
|
||||
metrics = {}
|
||||
|
||||
predicted_img = batch[self.image_to_discriminator].detach()
|
||||
self.adversarial_loss.pre_discriminator_step(real_batch=batch['image'], fake_batch=predicted_img,
|
||||
generator=self.generator, discriminator=self.discriminator)
|
||||
discr_real_pred, discr_real_features = self.discriminator(batch['image'])
|
||||
discr_fake_pred, discr_fake_features = self.discriminator(predicted_img)
|
||||
adv_discr_loss, adv_metrics = self.adversarial_loss.discriminator_loss(real_batch=batch['image'],
|
||||
fake_batch=predicted_img,
|
||||
discr_real_pred=discr_real_pred,
|
||||
discr_fake_pred=discr_fake_pred,
|
||||
mask=batch['mask'])
|
||||
total_loss = total_loss + adv_discr_loss
|
||||
metrics['discr_adv'] = adv_discr_loss
|
||||
metrics.update(add_prefix_to_keys(adv_metrics, 'adv_'))
|
||||
|
||||
|
||||
if batch.get('use_fake_fakes', False):
|
||||
fake_fakes = batch['fake_fakes']
|
||||
self.adversarial_loss.pre_discriminator_step(real_batch=batch['image'], fake_batch=fake_fakes,
|
||||
generator=self.generator, discriminator=self.discriminator)
|
||||
discr_fake_fakes_pred, _ = self.discriminator(fake_fakes)
|
||||
fake_fakes_adv_discr_loss, fake_fakes_adv_metrics = self.adversarial_loss.discriminator_loss(
|
||||
real_batch=batch['image'],
|
||||
fake_batch=fake_fakes,
|
||||
discr_real_pred=discr_real_pred,
|
||||
discr_fake_pred=discr_fake_fakes_pred,
|
||||
mask=batch['mask']
|
||||
)
|
||||
total_loss = total_loss + fake_fakes_adv_discr_loss
|
||||
metrics['discr_adv_fake_fakes'] = fake_fakes_adv_discr_loss
|
||||
metrics.update(add_prefix_to_keys(fake_fakes_adv_metrics, 'adv_'))
|
||||
|
||||
return total_loss, metrics
|
||||
@@ -0,0 +1,15 @@
|
||||
import logging
|
||||
|
||||
from saicinpainting.training.visualizers.directory import DirectoryVisualizer
|
||||
from saicinpainting.training.visualizers.noop import NoopVisualizer
|
||||
|
||||
|
||||
def make_visualizer(kind, **kwargs):
|
||||
logging.info(f'Make visualizer {kind}')
|
||||
|
||||
if kind == 'directory':
|
||||
return DirectoryVisualizer(**kwargs)
|
||||
if kind == 'noop':
|
||||
return NoopVisualizer()
|
||||
|
||||
raise ValueError(f'Unknown visualizer kind {kind}')
|
||||
@@ -0,0 +1,73 @@
|
||||
import abc
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from skimage import color
|
||||
from skimage.segmentation import mark_boundaries
|
||||
|
||||
from . import colors
|
||||
|
||||
COLORS, _ = colors.generate_colors(151) # 151 - max classes for semantic segmentation
|
||||
|
||||
|
||||
class BaseVisualizer:
|
||||
@abc.abstractmethod
|
||||
def __call__(self, epoch_i, batch_i, batch, suffix='', rank=None):
|
||||
"""
|
||||
Take a batch, make an image from it and visualize
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def visualize_mask_and_images(images_dict: Dict[str, np.ndarray], keys: List[str],
|
||||
last_without_mask=True, rescale_keys=None, mask_only_first=None,
|
||||
black_mask=False) -> np.ndarray:
|
||||
mask = images_dict['mask'] > 0.5
|
||||
result = []
|
||||
for i, k in enumerate(keys):
|
||||
img = images_dict[k]
|
||||
img = np.transpose(img, (1, 2, 0))
|
||||
|
||||
if rescale_keys is not None and k in rescale_keys:
|
||||
img = img - img.min()
|
||||
img /= img.max() + 1e-5
|
||||
if len(img.shape) == 2:
|
||||
img = np.expand_dims(img, 2)
|
||||
|
||||
if img.shape[2] == 1:
|
||||
img = np.repeat(img, 3, axis=2)
|
||||
elif (img.shape[2] > 3):
|
||||
img_classes = img.argmax(2)
|
||||
img = color.label2rgb(img_classes, colors=COLORS)
|
||||
|
||||
if mask_only_first:
|
||||
need_mark_boundaries = i == 0
|
||||
else:
|
||||
need_mark_boundaries = i < len(keys) - 1 or not last_without_mask
|
||||
|
||||
if need_mark_boundaries:
|
||||
if black_mask:
|
||||
img = img * (1 - mask[0][..., None])
|
||||
img = mark_boundaries(img,
|
||||
mask[0],
|
||||
color=(1., 0., 0.),
|
||||
outline_color=(1., 1., 1.),
|
||||
mode='thick')
|
||||
result.append(img)
|
||||
return np.concatenate(result, axis=1)
|
||||
|
||||
|
||||
def visualize_mask_and_images_batch(batch: Dict[str, torch.Tensor], keys: List[str], max_items=10,
|
||||
last_without_mask=True, rescale_keys=None) -> np.ndarray:
|
||||
batch = {k: tens.detach().cpu().numpy() for k, tens in batch.items()
|
||||
if k in keys or k == 'mask'}
|
||||
|
||||
batch_size = next(iter(batch.values())).shape[0]
|
||||
items_to_vis = min(batch_size, max_items)
|
||||
result = []
|
||||
for i in range(items_to_vis):
|
||||
cur_dct = {k: tens[i] for k, tens in batch.items()}
|
||||
result.append(visualize_mask_and_images(cur_dct, keys, last_without_mask=last_without_mask,
|
||||
rescale_keys=rescale_keys))
|
||||
return np.concatenate(result, axis=0)
|
||||
@@ -0,0 +1,76 @@
|
||||
import random
|
||||
import colorsys
|
||||
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use('agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
|
||||
|
||||
def generate_colors(nlabels, type='bright', first_color_black=False, last_color_black=True, verbose=False):
|
||||
# https://stackoverflow.com/questions/14720331/how-to-generate-random-colors-in-matplotlib
|
||||
"""
|
||||
Creates a random colormap to be used together with matplotlib. Useful for segmentation tasks
|
||||
:param nlabels: Number of labels (size of colormap)
|
||||
:param type: 'bright' for strong colors, 'soft' for pastel colors
|
||||
:param first_color_black: Option to use first color as black, True or False
|
||||
:param last_color_black: Option to use last color as black, True or False
|
||||
:param verbose: Prints the number of labels and shows the colormap. True or False
|
||||
:return: colormap for matplotlib
|
||||
"""
|
||||
if type not in ('bright', 'soft'):
|
||||
print ('Please choose "bright" or "soft" for type')
|
||||
return
|
||||
|
||||
if verbose:
|
||||
print('Number of labels: ' + str(nlabels))
|
||||
|
||||
# Generate color map for bright colors, based on hsv
|
||||
if type == 'bright':
|
||||
randHSVcolors = [(np.random.uniform(low=0.0, high=1),
|
||||
np.random.uniform(low=0.2, high=1),
|
||||
np.random.uniform(low=0.9, high=1)) for i in range(nlabels)]
|
||||
|
||||
# Convert HSV list to RGB
|
||||
randRGBcolors = []
|
||||
for HSVcolor in randHSVcolors:
|
||||
randRGBcolors.append(colorsys.hsv_to_rgb(HSVcolor[0], HSVcolor[1], HSVcolor[2]))
|
||||
|
||||
if first_color_black:
|
||||
randRGBcolors[0] = [0, 0, 0]
|
||||
|
||||
if last_color_black:
|
||||
randRGBcolors[-1] = [0, 0, 0]
|
||||
|
||||
random_colormap = LinearSegmentedColormap.from_list('new_map', randRGBcolors, N=nlabels)
|
||||
|
||||
# Generate soft pastel colors, by limiting the RGB spectrum
|
||||
if type == 'soft':
|
||||
low = 0.6
|
||||
high = 0.95
|
||||
randRGBcolors = [(np.random.uniform(low=low, high=high),
|
||||
np.random.uniform(low=low, high=high),
|
||||
np.random.uniform(low=low, high=high)) for i in range(nlabels)]
|
||||
|
||||
if first_color_black:
|
||||
randRGBcolors[0] = [0, 0, 0]
|
||||
|
||||
if last_color_black:
|
||||
randRGBcolors[-1] = [0, 0, 0]
|
||||
random_colormap = LinearSegmentedColormap.from_list('new_map', randRGBcolors, N=nlabels)
|
||||
|
||||
# Display colorbar
|
||||
if verbose:
|
||||
from matplotlib import colors, colorbar
|
||||
from matplotlib import pyplot as plt
|
||||
fig, ax = plt.subplots(1, 1, figsize=(15, 0.5))
|
||||
|
||||
bounds = np.linspace(0, nlabels, nlabels + 1)
|
||||
norm = colors.BoundaryNorm(bounds, nlabels)
|
||||
|
||||
cb = colorbar.ColorbarBase(ax, cmap=random_colormap, norm=norm, spacing='proportional', ticks=None,
|
||||
boundaries=bounds, format='%1i', orientation=u'horizontal')
|
||||
|
||||
return randRGBcolors, random_colormap
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from saicinpainting.training.visualizers.base import BaseVisualizer, visualize_mask_and_images_batch
|
||||
from saicinpainting.utils import check_and_warn_input_range
|
||||
|
||||
|
||||
class DirectoryVisualizer(BaseVisualizer):
|
||||
DEFAULT_KEY_ORDER = 'image predicted_image inpainted'.split(' ')
|
||||
|
||||
def __init__(self, outdir, key_order=DEFAULT_KEY_ORDER, max_items_in_batch=10,
|
||||
last_without_mask=True, rescale_keys=None):
|
||||
self.outdir = outdir
|
||||
os.makedirs(self.outdir, exist_ok=True)
|
||||
self.key_order = key_order
|
||||
self.max_items_in_batch = max_items_in_batch
|
||||
self.last_without_mask = last_without_mask
|
||||
self.rescale_keys = rescale_keys
|
||||
|
||||
def __call__(self, epoch_i, batch_i, batch, suffix='', rank=None):
|
||||
check_and_warn_input_range(batch['image'], 0, 1, 'DirectoryVisualizer target image')
|
||||
vis_img = visualize_mask_and_images_batch(batch, self.key_order, max_items=self.max_items_in_batch,
|
||||
last_without_mask=self.last_without_mask,
|
||||
rescale_keys=self.rescale_keys)
|
||||
|
||||
vis_img = np.clip(vis_img * 255, 0, 255).astype('uint8')
|
||||
|
||||
curoutdir = os.path.join(self.outdir, f'epoch{epoch_i:04d}{suffix}')
|
||||
os.makedirs(curoutdir, exist_ok=True)
|
||||
rank_suffix = f'_r{rank}' if rank is not None else ''
|
||||
out_fname = os.path.join(curoutdir, f'batch{batch_i:07d}{rank_suffix}.jpg')
|
||||
|
||||
vis_img = cv2.cvtColor(vis_img, cv2.COLOR_RGB2BGR)
|
||||
cv2.imwrite(out_fname, vis_img)
|
||||
@@ -0,0 +1,9 @@
|
||||
from saicinpainting.training.visualizers.base import BaseVisualizer
|
||||
|
||||
|
||||
class NoopVisualizer(BaseVisualizer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __call__(self, epoch_i, batch_i, batch, suffix='', rank=None):
|
||||
pass
|
||||
@@ -0,0 +1,177 @@
|
||||
import bisect
|
||||
import functools
|
||||
import logging
|
||||
import numbers
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
import torch
|
||||
from pytorch_lightning import seed_everything
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
import platform
|
||||
if platform.system() != 'Linux':
|
||||
signal.SIGUSR1 = 1
|
||||
|
||||
def check_and_warn_input_range(tensor, min_value, max_value, name):
|
||||
actual_min = tensor.min()
|
||||
actual_max = tensor.max()
|
||||
if actual_min < min_value or actual_max > max_value:
|
||||
warnings.warn(f"{name} must be in {min_value}..{max_value} range, but it ranges {actual_min}..{actual_max}")
|
||||
|
||||
|
||||
def sum_dict_with_prefix(target, cur_dict, prefix, default=0):
|
||||
for k, v in cur_dict.items():
|
||||
target_key = prefix + k
|
||||
target[target_key] = target.get(target_key, default) + v
|
||||
|
||||
|
||||
def average_dicts(dict_list):
|
||||
result = {}
|
||||
norm = 1e-3
|
||||
for dct in dict_list:
|
||||
sum_dict_with_prefix(result, dct, '')
|
||||
norm += 1
|
||||
for k in list(result):
|
||||
result[k] /= norm
|
||||
return result
|
||||
|
||||
|
||||
def add_prefix_to_keys(dct, prefix):
|
||||
return {prefix + k: v for k, v in dct.items()}
|
||||
|
||||
|
||||
def set_requires_grad(module, value):
|
||||
for param in module.parameters():
|
||||
param.requires_grad = value
|
||||
|
||||
|
||||
def flatten_dict(dct):
|
||||
result = {}
|
||||
for k, v in dct.items():
|
||||
if isinstance(k, tuple):
|
||||
k = '_'.join(k)
|
||||
if isinstance(v, dict):
|
||||
for sub_k, sub_v in flatten_dict(v).items():
|
||||
result[f'{k}_{sub_k}'] = sub_v
|
||||
else:
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
|
||||
class LinearRamp:
|
||||
def __init__(self, start_value=0, end_value=1, start_iter=-1, end_iter=0):
|
||||
self.start_value = start_value
|
||||
self.end_value = end_value
|
||||
self.start_iter = start_iter
|
||||
self.end_iter = end_iter
|
||||
|
||||
def __call__(self, i):
|
||||
if i < self.start_iter:
|
||||
return self.start_value
|
||||
if i >= self.end_iter:
|
||||
return self.end_value
|
||||
part = (i - self.start_iter) / (self.end_iter - self.start_iter)
|
||||
return self.start_value * (1 - part) + self.end_value * part
|
||||
|
||||
|
||||
class LadderRamp:
|
||||
def __init__(self, start_iters, values):
|
||||
self.start_iters = start_iters
|
||||
self.values = values
|
||||
assert len(values) == len(start_iters) + 1, (len(values), len(start_iters))
|
||||
|
||||
def __call__(self, i):
|
||||
segment_i = bisect.bisect_right(self.start_iters, i)
|
||||
return self.values[segment_i]
|
||||
|
||||
|
||||
def get_ramp(kind='ladder', **kwargs):
|
||||
if kind == 'linear':
|
||||
return LinearRamp(**kwargs)
|
||||
if kind == 'ladder':
|
||||
return LadderRamp(**kwargs)
|
||||
raise ValueError(f'Unexpected ramp kind: {kind}')
|
||||
|
||||
|
||||
def print_traceback_handler(sig, frame):
|
||||
LOGGER.warning(f'Received signal {sig}')
|
||||
bt = ''.join(traceback.format_stack())
|
||||
LOGGER.warning(f'Requested stack trace:\n{bt}')
|
||||
|
||||
|
||||
def register_debug_signal_handlers(sig=signal.SIGUSR1, handler=print_traceback_handler):
|
||||
LOGGER.warning(f'Setting signal {sig} handler {handler}')
|
||||
signal.signal(sig, handler)
|
||||
|
||||
|
||||
def handle_deterministic_config(config):
|
||||
seed = dict(config).get('seed', None)
|
||||
if seed is None:
|
||||
return False
|
||||
|
||||
seed_everything(seed)
|
||||
return True
|
||||
|
||||
|
||||
def get_shape(t):
|
||||
if torch.is_tensor(t):
|
||||
return tuple(t.shape)
|
||||
elif isinstance(t, dict):
|
||||
return {n: get_shape(q) for n, q in t.items()}
|
||||
elif isinstance(t, (list, tuple)):
|
||||
return [get_shape(q) for q in t]
|
||||
elif isinstance(t, numbers.Number):
|
||||
return type(t)
|
||||
else:
|
||||
raise ValueError('unexpected type {}'.format(type(t)))
|
||||
|
||||
|
||||
def get_has_ddp_rank():
|
||||
master_port = os.environ.get('MASTER_PORT', None)
|
||||
node_rank = os.environ.get('NODE_RANK', None)
|
||||
local_rank = os.environ.get('LOCAL_RANK', None)
|
||||
world_size = os.environ.get('WORLD_SIZE', None)
|
||||
has_rank = master_port is not None or node_rank is not None or local_rank is not None or world_size is not None
|
||||
return has_rank
|
||||
|
||||
|
||||
def handle_ddp_subprocess():
|
||||
def main_decorator(main_func):
|
||||
@functools.wraps(main_func)
|
||||
def new_main(*args, **kwargs):
|
||||
# Trainer sets MASTER_PORT, NODE_RANK, LOCAL_RANK, WORLD_SIZE
|
||||
parent_cwd = os.environ.get('TRAINING_PARENT_WORK_DIR', None)
|
||||
has_parent = parent_cwd is not None
|
||||
has_rank = get_has_ddp_rank()
|
||||
assert has_parent == has_rank, f'Inconsistent state: has_parent={has_parent}, has_rank={has_rank}'
|
||||
|
||||
if has_parent:
|
||||
# we are in the worker
|
||||
sys.argv.extend([
|
||||
f'hydra.run.dir={parent_cwd}',
|
||||
# 'hydra/hydra_logging=disabled',
|
||||
# 'hydra/job_logging=disabled'
|
||||
])
|
||||
# do nothing if this is a top-level process
|
||||
# TRAINING_PARENT_WORK_DIR is set in handle_ddp_parent_process after hydra initialization
|
||||
|
||||
main_func(*args, **kwargs)
|
||||
return new_main
|
||||
return main_decorator
|
||||
|
||||
|
||||
def handle_ddp_parent_process():
|
||||
parent_cwd = os.environ.get('TRAINING_PARENT_WORK_DIR', None)
|
||||
has_parent = parent_cwd is not None
|
||||
has_rank = get_has_ddp_rank()
|
||||
assert has_parent == has_rank, f'Inconsistent state: has_parent={has_parent}, has_rank={has_rank}'
|
||||
|
||||
if parent_cwd is None:
|
||||
os.environ['TRAINING_PARENT_WORK_DIR'] = os.getcwd()
|
||||
|
||||
return has_parent
|
||||