Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e870e79321 | ||
|
|
4264042d74 | ||
|
|
98704c48d0 | ||
|
|
0d12cad0e5 | ||
|
|
a39d634423 | ||
|
|
43f5242c2c | ||
|
|
9243b7abf1 | ||
|
|
3564cd3404 | ||
|
|
831ae70608 | ||
|
|
a8a5827ca5 | ||
|
|
5674347aaa | ||
|
|
8d05dddca6 | ||
|
|
a3917044ef | ||
|
|
c92d603c9f | ||
|
|
8bdb4cbbc3 | ||
|
|
2dc07818f7 | ||
|
|
7587b2b865 | ||
|
|
1cdf3771f8 | ||
|
|
be106b53ac | ||
|
|
21b1b42dfb | ||
|
|
c9dbec728d | ||
|
|
7c8199661e | ||
|
|
415215419d | ||
|
|
ead6ac07a5 | ||
|
|
b93c6de49d | ||
|
|
4b2a3779de | ||
|
|
3f4949c057 | ||
|
|
c7c7276ae8 | ||
|
|
a1b8c2871b |
No files matched your search
@@ -0,0 +1,131 @@
|
||||
.vscode
|
||||
|
||||
# ignored files
|
||||
version.py
|
||||
|
||||
# ignored files with suffix
|
||||
*.html
|
||||
# *.png
|
||||
# *.jpeg
|
||||
# *.jpg
|
||||
# *.gif
|
||||
*.pt
|
||||
*.pth
|
||||
*.dat
|
||||
*.zip
|
||||
|
||||
# template
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# project
|
||||
experiments_model/
|
||||
unreleased/
|
||||
results_eval/
|
||||
results/
|
||||
*debug*
|
||||
*old*
|
||||
*.sh
|
||||
@@ -0,0 +1,14 @@
|
||||
# S-Lab License 1.0
|
||||
|
||||
Copyright 2023 S-Lab
|
||||
|
||||
Redistribution and use for non-commercial purpose in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
4. In the event that redistribution and/or use for commercial purpose in source or binary forms, with or without modification is required, please contact the contributor(s) of the work.
|
||||
|
||||
|
||||
---
|
||||
For inquiries or to obtain permission for commercial use, please consult Dr. Shangchen Zhou (shangchenzhou@gmail.com) and Prof. Chen Change Loy (ccloy@ntu.edu.sg).
|
||||
@@ -0,0 +1,2 @@
|
||||
# from .demo import RAFT_infer
|
||||
from .raft import RAFT
|
||||
@@ -0,0 +1,111 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from .utils.utils import bilinear_sampler, coords_grid
|
||||
|
||||
try:
|
||||
import alt_cuda_corr
|
||||
except:
|
||||
# alt_cuda_corr is not compiled
|
||||
pass
|
||||
|
||||
|
||||
class CorrBlock:
|
||||
def __init__(self, fmap1, fmap2, num_levels=4, radius=4):
|
||||
self.num_levels = num_levels
|
||||
self.radius = radius
|
||||
self.corr_pyramid = []
|
||||
|
||||
# all pairs correlation
|
||||
corr = CorrBlock.corr(fmap1, fmap2)
|
||||
|
||||
batch, h1, w1, dim, h2, w2 = corr.shape
|
||||
corr = corr.reshape(batch*h1*w1, dim, h2, w2)
|
||||
|
||||
self.corr_pyramid.append(corr)
|
||||
for i in range(self.num_levels-1):
|
||||
corr = F.avg_pool2d(corr, 2, stride=2)
|
||||
self.corr_pyramid.append(corr)
|
||||
|
||||
def __call__(self, coords):
|
||||
r = self.radius
|
||||
coords = coords.permute(0, 2, 3, 1)
|
||||
batch, h1, w1, _ = coords.shape
|
||||
|
||||
out_pyramid = []
|
||||
for i in range(self.num_levels):
|
||||
corr = self.corr_pyramid[i]
|
||||
dx = torch.linspace(-r, r, 2*r+1)
|
||||
dy = torch.linspace(-r, r, 2*r+1)
|
||||
delta = torch.stack(torch.meshgrid(dy, dx), axis=-1).to(coords.device)
|
||||
|
||||
centroid_lvl = coords.reshape(batch*h1*w1, 1, 1, 2) / 2**i
|
||||
delta_lvl = delta.view(1, 2*r+1, 2*r+1, 2)
|
||||
coords_lvl = centroid_lvl + delta_lvl
|
||||
|
||||
corr = bilinear_sampler(corr, coords_lvl)
|
||||
corr = corr.view(batch, h1, w1, -1)
|
||||
out_pyramid.append(corr)
|
||||
|
||||
out = torch.cat(out_pyramid, dim=-1)
|
||||
return out.permute(0, 3, 1, 2).contiguous().float()
|
||||
|
||||
@staticmethod
|
||||
def corr(fmap1, fmap2):
|
||||
batch, dim, ht, wd = fmap1.shape
|
||||
fmap1 = fmap1.view(batch, dim, ht*wd)
|
||||
fmap2 = fmap2.view(batch, dim, ht*wd)
|
||||
|
||||
corr = torch.matmul(fmap1.transpose(1,2), fmap2)
|
||||
corr = corr.view(batch, ht, wd, 1, ht, wd)
|
||||
return corr / torch.sqrt(torch.tensor(dim).float())
|
||||
|
||||
|
||||
class CorrLayer(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, fmap1, fmap2, coords, r):
|
||||
fmap1 = fmap1.contiguous()
|
||||
fmap2 = fmap2.contiguous()
|
||||
coords = coords.contiguous()
|
||||
ctx.save_for_backward(fmap1, fmap2, coords)
|
||||
ctx.r = r
|
||||
corr, = correlation_cudaz.forward(fmap1, fmap2, coords, ctx.r)
|
||||
return corr
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_corr):
|
||||
fmap1, fmap2, coords = ctx.saved_tensors
|
||||
grad_corr = grad_corr.contiguous()
|
||||
fmap1_grad, fmap2_grad, coords_grad = \
|
||||
correlation_cudaz.backward(fmap1, fmap2, coords, grad_corr, ctx.r)
|
||||
return fmap1_grad, fmap2_grad, coords_grad, None
|
||||
|
||||
|
||||
class AlternateCorrBlock:
|
||||
def __init__(self, fmap1, fmap2, num_levels=4, radius=4):
|
||||
self.num_levels = num_levels
|
||||
self.radius = radius
|
||||
|
||||
self.pyramid = [(fmap1, fmap2)]
|
||||
for i in range(self.num_levels):
|
||||
fmap1 = F.avg_pool2d(fmap1, 2, stride=2)
|
||||
fmap2 = F.avg_pool2d(fmap2, 2, stride=2)
|
||||
self.pyramid.append((fmap1, fmap2))
|
||||
|
||||
def __call__(self, coords):
|
||||
|
||||
coords = coords.permute(0, 2, 3, 1)
|
||||
B, H, W, _ = coords.shape
|
||||
|
||||
corr_list = []
|
||||
for i in range(self.num_levels):
|
||||
r = self.radius
|
||||
fmap1_i = self.pyramid[0][0].permute(0, 2, 3, 1)
|
||||
fmap2_i = self.pyramid[i][1].permute(0, 2, 3, 1)
|
||||
|
||||
coords_i = (coords / 2**i).reshape(B, 1, H, W, 2).contiguous()
|
||||
corr = alt_cuda_corr(fmap1_i, fmap2_i, coords_i, r)
|
||||
corr_list.append(corr.squeeze(1))
|
||||
|
||||
corr = torch.stack(corr_list, dim=1)
|
||||
corr = corr.reshape(B, -1, H, W)
|
||||
return corr / 16.0
|
||||
@@ -0,0 +1,235 @@
|
||||
# Data loading based on https://github.com/NVIDIA/flownet2-pytorch
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.utils.data as data
|
||||
import torch.nn.functional as F
|
||||
|
||||
import os
|
||||
import math
|
||||
import random
|
||||
from glob import glob
|
||||
import os.path as osp
|
||||
|
||||
from utils import frame_utils
|
||||
from utils.augmentor import FlowAugmentor, SparseFlowAugmentor
|
||||
|
||||
|
||||
class FlowDataset(data.Dataset):
|
||||
def __init__(self, aug_params=None, sparse=False):
|
||||
self.augmentor = None
|
||||
self.sparse = sparse
|
||||
if aug_params is not None:
|
||||
if sparse:
|
||||
self.augmentor = SparseFlowAugmentor(**aug_params)
|
||||
else:
|
||||
self.augmentor = FlowAugmentor(**aug_params)
|
||||
|
||||
self.is_test = False
|
||||
self.init_seed = False
|
||||
self.flow_list = []
|
||||
self.image_list = []
|
||||
self.extra_info = []
|
||||
|
||||
def __getitem__(self, index):
|
||||
|
||||
if self.is_test:
|
||||
img1 = frame_utils.read_gen(self.image_list[index][0])
|
||||
img2 = frame_utils.read_gen(self.image_list[index][1])
|
||||
img1 = np.array(img1).astype(np.uint8)[..., :3]
|
||||
img2 = np.array(img2).astype(np.uint8)[..., :3]
|
||||
img1 = torch.from_numpy(img1).permute(2, 0, 1).float()
|
||||
img2 = torch.from_numpy(img2).permute(2, 0, 1).float()
|
||||
return img1, img2, self.extra_info[index]
|
||||
|
||||
if not self.init_seed:
|
||||
worker_info = torch.utils.data.get_worker_info()
|
||||
if worker_info is not None:
|
||||
torch.manual_seed(worker_info.id)
|
||||
np.random.seed(worker_info.id)
|
||||
random.seed(worker_info.id)
|
||||
self.init_seed = True
|
||||
|
||||
index = index % len(self.image_list)
|
||||
valid = None
|
||||
if self.sparse:
|
||||
flow, valid = frame_utils.readFlowKITTI(self.flow_list[index])
|
||||
else:
|
||||
flow = frame_utils.read_gen(self.flow_list[index])
|
||||
|
||||
img1 = frame_utils.read_gen(self.image_list[index][0])
|
||||
img2 = frame_utils.read_gen(self.image_list[index][1])
|
||||
|
||||
flow = np.array(flow).astype(np.float32)
|
||||
img1 = np.array(img1).astype(np.uint8)
|
||||
img2 = np.array(img2).astype(np.uint8)
|
||||
|
||||
# grayscale images
|
||||
if len(img1.shape) == 2:
|
||||
img1 = np.tile(img1[...,None], (1, 1, 3))
|
||||
img2 = np.tile(img2[...,None], (1, 1, 3))
|
||||
else:
|
||||
img1 = img1[..., :3]
|
||||
img2 = img2[..., :3]
|
||||
|
||||
if self.augmentor is not None:
|
||||
if self.sparse:
|
||||
img1, img2, flow, valid = self.augmentor(img1, img2, flow, valid)
|
||||
else:
|
||||
img1, img2, flow = self.augmentor(img1, img2, flow)
|
||||
|
||||
img1 = torch.from_numpy(img1).permute(2, 0, 1).float()
|
||||
img2 = torch.from_numpy(img2).permute(2, 0, 1).float()
|
||||
flow = torch.from_numpy(flow).permute(2, 0, 1).float()
|
||||
|
||||
if valid is not None:
|
||||
valid = torch.from_numpy(valid)
|
||||
else:
|
||||
valid = (flow[0].abs() < 1000) & (flow[1].abs() < 1000)
|
||||
|
||||
return img1, img2, flow, valid.float()
|
||||
|
||||
|
||||
def __rmul__(self, v):
|
||||
self.flow_list = v * self.flow_list
|
||||
self.image_list = v * self.image_list
|
||||
return self
|
||||
|
||||
def __len__(self):
|
||||
return len(self.image_list)
|
||||
|
||||
|
||||
class MpiSintel(FlowDataset):
|
||||
def __init__(self, aug_params=None, split='training', root='datasets/Sintel', dstype='clean'):
|
||||
super(MpiSintel, self).__init__(aug_params)
|
||||
flow_root = osp.join(root, split, 'flow')
|
||||
image_root = osp.join(root, split, dstype)
|
||||
|
||||
if split == 'test':
|
||||
self.is_test = True
|
||||
|
||||
for scene in os.listdir(image_root):
|
||||
image_list = sorted(glob(osp.join(image_root, scene, '*.png')))
|
||||
for i in range(len(image_list)-1):
|
||||
self.image_list += [ [image_list[i], image_list[i+1]] ]
|
||||
self.extra_info += [ (scene, i) ] # scene and frame_id
|
||||
|
||||
if split != 'test':
|
||||
self.flow_list += sorted(glob(osp.join(flow_root, scene, '*.flo')))
|
||||
|
||||
|
||||
class FlyingChairs(FlowDataset):
|
||||
def __init__(self, aug_params=None, split='train', root='datasets/FlyingChairs_release/data'):
|
||||
super(FlyingChairs, self).__init__(aug_params)
|
||||
|
||||
images = sorted(glob(osp.join(root, '*.ppm')))
|
||||
flows = sorted(glob(osp.join(root, '*.flo')))
|
||||
assert (len(images)//2 == len(flows))
|
||||
|
||||
split_list = np.loadtxt('chairs_split.txt', dtype=np.int32)
|
||||
for i in range(len(flows)):
|
||||
xid = split_list[i]
|
||||
if (split=='training' and xid==1) or (split=='validation' and xid==2):
|
||||
self.flow_list += [ flows[i] ]
|
||||
self.image_list += [ [images[2*i], images[2*i+1]] ]
|
||||
|
||||
|
||||
class FlyingThings3D(FlowDataset):
|
||||
def __init__(self, aug_params=None, root='datasets/FlyingThings3D', dstype='frames_cleanpass'):
|
||||
super(FlyingThings3D, self).__init__(aug_params)
|
||||
|
||||
for cam in ['left']:
|
||||
for direction in ['into_future', 'into_past']:
|
||||
image_dirs = sorted(glob(osp.join(root, dstype, 'TRAIN/*/*')))
|
||||
image_dirs = sorted([osp.join(f, cam) for f in image_dirs])
|
||||
|
||||
flow_dirs = sorted(glob(osp.join(root, 'optical_flow/TRAIN/*/*')))
|
||||
flow_dirs = sorted([osp.join(f, direction, cam) for f in flow_dirs])
|
||||
|
||||
for idir, fdir in zip(image_dirs, flow_dirs):
|
||||
images = sorted(glob(osp.join(idir, '*.png')) )
|
||||
flows = sorted(glob(osp.join(fdir, '*.pfm')) )
|
||||
for i in range(len(flows)-1):
|
||||
if direction == 'into_future':
|
||||
self.image_list += [ [images[i], images[i+1]] ]
|
||||
self.flow_list += [ flows[i] ]
|
||||
elif direction == 'into_past':
|
||||
self.image_list += [ [images[i+1], images[i]] ]
|
||||
self.flow_list += [ flows[i+1] ]
|
||||
|
||||
|
||||
class KITTI(FlowDataset):
|
||||
def __init__(self, aug_params=None, split='training', root='datasets/KITTI'):
|
||||
super(KITTI, self).__init__(aug_params, sparse=True)
|
||||
if split == 'testing':
|
||||
self.is_test = True
|
||||
|
||||
root = osp.join(root, split)
|
||||
images1 = sorted(glob(osp.join(root, 'image_2/*_10.png')))
|
||||
images2 = sorted(glob(osp.join(root, 'image_2/*_11.png')))
|
||||
|
||||
for img1, img2 in zip(images1, images2):
|
||||
frame_id = img1.split('/')[-1]
|
||||
self.extra_info += [ [frame_id] ]
|
||||
self.image_list += [ [img1, img2] ]
|
||||
|
||||
if split == 'training':
|
||||
self.flow_list = sorted(glob(osp.join(root, 'flow_occ/*_10.png')))
|
||||
|
||||
|
||||
class HD1K(FlowDataset):
|
||||
def __init__(self, aug_params=None, root='datasets/HD1k'):
|
||||
super(HD1K, self).__init__(aug_params, sparse=True)
|
||||
|
||||
seq_ix = 0
|
||||
while 1:
|
||||
flows = sorted(glob(os.path.join(root, 'hd1k_flow_gt', 'flow_occ/%06d_*.png' % seq_ix)))
|
||||
images = sorted(glob(os.path.join(root, 'hd1k_input', 'image_2/%06d_*.png' % seq_ix)))
|
||||
|
||||
if len(flows) == 0:
|
||||
break
|
||||
|
||||
for i in range(len(flows)-1):
|
||||
self.flow_list += [flows[i]]
|
||||
self.image_list += [ [images[i], images[i+1]] ]
|
||||
|
||||
seq_ix += 1
|
||||
|
||||
|
||||
def fetch_dataloader(args, TRAIN_DS='C+T+K+S+H'):
|
||||
""" Create the data loader for the corresponding trainign set """
|
||||
|
||||
if args.stage == 'chairs':
|
||||
aug_params = {'crop_size': args.image_size, 'min_scale': -0.1, 'max_scale': 1.0, 'do_flip': True}
|
||||
train_dataset = FlyingChairs(aug_params, split='training')
|
||||
|
||||
elif args.stage == 'things':
|
||||
aug_params = {'crop_size': args.image_size, 'min_scale': -0.4, 'max_scale': 0.8, 'do_flip': True}
|
||||
clean_dataset = FlyingThings3D(aug_params, dstype='frames_cleanpass')
|
||||
final_dataset = FlyingThings3D(aug_params, dstype='frames_finalpass')
|
||||
train_dataset = clean_dataset + final_dataset
|
||||
|
||||
elif args.stage == 'sintel':
|
||||
aug_params = {'crop_size': args.image_size, 'min_scale': -0.2, 'max_scale': 0.6, 'do_flip': True}
|
||||
things = FlyingThings3D(aug_params, dstype='frames_cleanpass')
|
||||
sintel_clean = MpiSintel(aug_params, split='training', dstype='clean')
|
||||
sintel_final = MpiSintel(aug_params, split='training', dstype='final')
|
||||
|
||||
if TRAIN_DS == 'C+T+K+S+H':
|
||||
kitti = KITTI({'crop_size': args.image_size, 'min_scale': -0.3, 'max_scale': 0.5, 'do_flip': True})
|
||||
hd1k = HD1K({'crop_size': args.image_size, 'min_scale': -0.5, 'max_scale': 0.2, 'do_flip': True})
|
||||
train_dataset = 100*sintel_clean + 100*sintel_final + 200*kitti + 5*hd1k + things
|
||||
|
||||
elif TRAIN_DS == 'C+T+K/S':
|
||||
train_dataset = 100*sintel_clean + 100*sintel_final + things
|
||||
|
||||
elif args.stage == 'kitti':
|
||||
aug_params = {'crop_size': args.image_size, 'min_scale': -0.2, 'max_scale': 0.4, 'do_flip': False}
|
||||
train_dataset = KITTI(aug_params, split='training')
|
||||
|
||||
train_loader = data.DataLoader(train_dataset, batch_size=args.batch_size,
|
||||
pin_memory=False, shuffle=True, num_workers=4, drop_last=True)
|
||||
|
||||
print('Training with %d image pairs' % len(train_dataset))
|
||||
return train_loader
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import cv2
|
||||
import glob
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
from .raft import RAFT
|
||||
from .utils import flow_viz
|
||||
from .utils.utils import InputPadder
|
||||
|
||||
|
||||
|
||||
DEVICE = 'cuda'
|
||||
|
||||
def load_image(imfile):
|
||||
img = np.array(Image.open(imfile)).astype(np.uint8)
|
||||
img = torch.from_numpy(img).permute(2, 0, 1).float()
|
||||
return img
|
||||
|
||||
|
||||
def load_image_list(image_files):
|
||||
images = []
|
||||
for imfile in sorted(image_files):
|
||||
images.append(load_image(imfile))
|
||||
|
||||
images = torch.stack(images, dim=0)
|
||||
images = images.to(DEVICE)
|
||||
|
||||
padder = InputPadder(images.shape)
|
||||
return padder.pad(images)[0]
|
||||
|
||||
|
||||
def viz(img, flo):
|
||||
img = img[0].permute(1,2,0).cpu().numpy()
|
||||
flo = flo[0].permute(1,2,0).cpu().numpy()
|
||||
|
||||
# map flow to rgb image
|
||||
flo = flow_viz.flow_to_image(flo)
|
||||
# img_flo = np.concatenate([img, flo], axis=0)
|
||||
img_flo = flo
|
||||
|
||||
cv2.imwrite('/home/chengao/test/flow.png', img_flo[:, :, [2,1,0]])
|
||||
# cv2.imshow('image', img_flo[:, :, [2,1,0]]/255.0)
|
||||
# cv2.waitKey()
|
||||
|
||||
|
||||
def demo(args):
|
||||
model = torch.nn.DataParallel(RAFT(args))
|
||||
model.load_state_dict(torch.load(args.model))
|
||||
|
||||
model = model.module
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
|
||||
with torch.no_grad():
|
||||
images = glob.glob(os.path.join(args.path, '*.png')) + \
|
||||
glob.glob(os.path.join(args.path, '*.jpg'))
|
||||
|
||||
images = load_image_list(images)
|
||||
for i in range(images.shape[0]-1):
|
||||
image1 = images[i,None]
|
||||
image2 = images[i+1,None]
|
||||
|
||||
flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
|
||||
viz(image1, flow_up)
|
||||
|
||||
|
||||
def RAFT_infer(args):
|
||||
model = torch.nn.DataParallel(RAFT(args))
|
||||
model.load_state_dict(torch.load(args.model))
|
||||
|
||||
model = model.module
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
|
||||
return model
|
||||
@@ -0,0 +1,267 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class ResidualBlock(nn.Module):
|
||||
def __init__(self, in_planes, planes, norm_fn='group', stride=1):
|
||||
super(ResidualBlock, self).__init__()
|
||||
|
||||
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, padding=1, stride=stride)
|
||||
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
|
||||
num_groups = planes // 8
|
||||
|
||||
if norm_fn == 'group':
|
||||
self.norm1 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
|
||||
self.norm2 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
|
||||
if not stride == 1:
|
||||
self.norm3 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
|
||||
|
||||
elif norm_fn == 'batch':
|
||||
self.norm1 = nn.BatchNorm2d(planes)
|
||||
self.norm2 = nn.BatchNorm2d(planes)
|
||||
if not stride == 1:
|
||||
self.norm3 = nn.BatchNorm2d(planes)
|
||||
|
||||
elif norm_fn == 'instance':
|
||||
self.norm1 = nn.InstanceNorm2d(planes)
|
||||
self.norm2 = nn.InstanceNorm2d(planes)
|
||||
if not stride == 1:
|
||||
self.norm3 = nn.InstanceNorm2d(planes)
|
||||
|
||||
elif norm_fn == 'none':
|
||||
self.norm1 = nn.Sequential()
|
||||
self.norm2 = nn.Sequential()
|
||||
if not stride == 1:
|
||||
self.norm3 = nn.Sequential()
|
||||
|
||||
if stride == 1:
|
||||
self.downsample = None
|
||||
|
||||
else:
|
||||
self.downsample = nn.Sequential(
|
||||
nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), self.norm3)
|
||||
|
||||
|
||||
def forward(self, x):
|
||||
y = x
|
||||
y = self.relu(self.norm1(self.conv1(y)))
|
||||
y = self.relu(self.norm2(self.conv2(y)))
|
||||
|
||||
if self.downsample is not None:
|
||||
x = self.downsample(x)
|
||||
|
||||
return self.relu(x+y)
|
||||
|
||||
|
||||
|
||||
class BottleneckBlock(nn.Module):
|
||||
def __init__(self, in_planes, planes, norm_fn='group', stride=1):
|
||||
super(BottleneckBlock, self).__init__()
|
||||
|
||||
self.conv1 = nn.Conv2d(in_planes, planes//4, kernel_size=1, padding=0)
|
||||
self.conv2 = nn.Conv2d(planes//4, planes//4, kernel_size=3, padding=1, stride=stride)
|
||||
self.conv3 = nn.Conv2d(planes//4, planes, kernel_size=1, padding=0)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
|
||||
num_groups = planes // 8
|
||||
|
||||
if norm_fn == 'group':
|
||||
self.norm1 = nn.GroupNorm(num_groups=num_groups, num_channels=planes//4)
|
||||
self.norm2 = nn.GroupNorm(num_groups=num_groups, num_channels=planes//4)
|
||||
self.norm3 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
|
||||
if not stride == 1:
|
||||
self.norm4 = nn.GroupNorm(num_groups=num_groups, num_channels=planes)
|
||||
|
||||
elif norm_fn == 'batch':
|
||||
self.norm1 = nn.BatchNorm2d(planes//4)
|
||||
self.norm2 = nn.BatchNorm2d(planes//4)
|
||||
self.norm3 = nn.BatchNorm2d(planes)
|
||||
if not stride == 1:
|
||||
self.norm4 = nn.BatchNorm2d(planes)
|
||||
|
||||
elif norm_fn == 'instance':
|
||||
self.norm1 = nn.InstanceNorm2d(planes//4)
|
||||
self.norm2 = nn.InstanceNorm2d(planes//4)
|
||||
self.norm3 = nn.InstanceNorm2d(planes)
|
||||
if not stride == 1:
|
||||
self.norm4 = nn.InstanceNorm2d(planes)
|
||||
|
||||
elif norm_fn == 'none':
|
||||
self.norm1 = nn.Sequential()
|
||||
self.norm2 = nn.Sequential()
|
||||
self.norm3 = nn.Sequential()
|
||||
if not stride == 1:
|
||||
self.norm4 = nn.Sequential()
|
||||
|
||||
if stride == 1:
|
||||
self.downsample = None
|
||||
|
||||
else:
|
||||
self.downsample = nn.Sequential(
|
||||
nn.Conv2d(in_planes, planes, kernel_size=1, stride=stride), self.norm4)
|
||||
|
||||
|
||||
def forward(self, x):
|
||||
y = x
|
||||
y = self.relu(self.norm1(self.conv1(y)))
|
||||
y = self.relu(self.norm2(self.conv2(y)))
|
||||
y = self.relu(self.norm3(self.conv3(y)))
|
||||
|
||||
if self.downsample is not None:
|
||||
x = self.downsample(x)
|
||||
|
||||
return self.relu(x+y)
|
||||
|
||||
class BasicEncoder(nn.Module):
|
||||
def __init__(self, output_dim=128, norm_fn='batch', dropout=0.0):
|
||||
super(BasicEncoder, self).__init__()
|
||||
self.norm_fn = norm_fn
|
||||
|
||||
if self.norm_fn == 'group':
|
||||
self.norm1 = nn.GroupNorm(num_groups=8, num_channels=64)
|
||||
|
||||
elif self.norm_fn == 'batch':
|
||||
self.norm1 = nn.BatchNorm2d(64)
|
||||
|
||||
elif self.norm_fn == 'instance':
|
||||
self.norm1 = nn.InstanceNorm2d(64)
|
||||
|
||||
elif self.norm_fn == 'none':
|
||||
self.norm1 = nn.Sequential()
|
||||
|
||||
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
|
||||
self.relu1 = nn.ReLU(inplace=True)
|
||||
|
||||
self.in_planes = 64
|
||||
self.layer1 = self._make_layer(64, stride=1)
|
||||
self.layer2 = self._make_layer(96, stride=2)
|
||||
self.layer3 = self._make_layer(128, stride=2)
|
||||
|
||||
# output convolution
|
||||
self.conv2 = nn.Conv2d(128, output_dim, kernel_size=1)
|
||||
|
||||
self.dropout = None
|
||||
if dropout > 0:
|
||||
self.dropout = nn.Dropout2d(p=dropout)
|
||||
|
||||
for m in self.modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
||||
elif isinstance(m, (nn.BatchNorm2d, nn.InstanceNorm2d, nn.GroupNorm)):
|
||||
if m.weight is not None:
|
||||
nn.init.constant_(m.weight, 1)
|
||||
if m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
def _make_layer(self, dim, stride=1):
|
||||
layer1 = ResidualBlock(self.in_planes, dim, self.norm_fn, stride=stride)
|
||||
layer2 = ResidualBlock(dim, dim, self.norm_fn, stride=1)
|
||||
layers = (layer1, layer2)
|
||||
|
||||
self.in_planes = dim
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
# if input is list, combine batch dimension
|
||||
is_list = isinstance(x, tuple) or isinstance(x, list)
|
||||
if is_list:
|
||||
batch_dim = x[0].shape[0]
|
||||
x = torch.cat(x, dim=0)
|
||||
|
||||
x = self.conv1(x)
|
||||
x = self.norm1(x)
|
||||
x = self.relu1(x)
|
||||
|
||||
x = self.layer1(x)
|
||||
x = self.layer2(x)
|
||||
x = self.layer3(x)
|
||||
|
||||
x = self.conv2(x)
|
||||
|
||||
if self.training and self.dropout is not None:
|
||||
x = self.dropout(x)
|
||||
|
||||
if is_list:
|
||||
x = torch.split(x, [batch_dim, batch_dim], dim=0)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class SmallEncoder(nn.Module):
|
||||
def __init__(self, output_dim=128, norm_fn='batch', dropout=0.0):
|
||||
super(SmallEncoder, self).__init__()
|
||||
self.norm_fn = norm_fn
|
||||
|
||||
if self.norm_fn == 'group':
|
||||
self.norm1 = nn.GroupNorm(num_groups=8, num_channels=32)
|
||||
|
||||
elif self.norm_fn == 'batch':
|
||||
self.norm1 = nn.BatchNorm2d(32)
|
||||
|
||||
elif self.norm_fn == 'instance':
|
||||
self.norm1 = nn.InstanceNorm2d(32)
|
||||
|
||||
elif self.norm_fn == 'none':
|
||||
self.norm1 = nn.Sequential()
|
||||
|
||||
self.conv1 = nn.Conv2d(3, 32, kernel_size=7, stride=2, padding=3)
|
||||
self.relu1 = nn.ReLU(inplace=True)
|
||||
|
||||
self.in_planes = 32
|
||||
self.layer1 = self._make_layer(32, stride=1)
|
||||
self.layer2 = self._make_layer(64, stride=2)
|
||||
self.layer3 = self._make_layer(96, stride=2)
|
||||
|
||||
self.dropout = None
|
||||
if dropout > 0:
|
||||
self.dropout = nn.Dropout2d(p=dropout)
|
||||
|
||||
self.conv2 = nn.Conv2d(96, output_dim, kernel_size=1)
|
||||
|
||||
for m in self.modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
||||
elif isinstance(m, (nn.BatchNorm2d, nn.InstanceNorm2d, nn.GroupNorm)):
|
||||
if m.weight is not None:
|
||||
nn.init.constant_(m.weight, 1)
|
||||
if m.bias is not None:
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
def _make_layer(self, dim, stride=1):
|
||||
layer1 = BottleneckBlock(self.in_planes, dim, self.norm_fn, stride=stride)
|
||||
layer2 = BottleneckBlock(dim, dim, self.norm_fn, stride=1)
|
||||
layers = (layer1, layer2)
|
||||
|
||||
self.in_planes = dim
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
# if input is list, combine batch dimension
|
||||
is_list = isinstance(x, tuple) or isinstance(x, list)
|
||||
if is_list:
|
||||
batch_dim = x[0].shape[0]
|
||||
x = torch.cat(x, dim=0)
|
||||
|
||||
x = self.conv1(x)
|
||||
x = self.norm1(x)
|
||||
x = self.relu1(x)
|
||||
|
||||
x = self.layer1(x)
|
||||
x = self.layer2(x)
|
||||
x = self.layer3(x)
|
||||
x = self.conv2(x)
|
||||
|
||||
if self.training and self.dropout is not None:
|
||||
x = self.dropout(x)
|
||||
|
||||
if is_list:
|
||||
x = torch.split(x, [batch_dim, batch_dim], dim=0)
|
||||
|
||||
return x
|
||||
@@ -0,0 +1,146 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .update import BasicUpdateBlock, SmallUpdateBlock
|
||||
from .extractor import BasicEncoder, SmallEncoder
|
||||
from .corr import CorrBlock, AlternateCorrBlock
|
||||
from .utils.utils import bilinear_sampler, coords_grid, upflow8
|
||||
|
||||
try:
|
||||
autocast = torch.cuda.amp.autocast
|
||||
except:
|
||||
# dummy autocast for PyTorch < 1.6
|
||||
class autocast:
|
||||
def __init__(self, enabled):
|
||||
pass
|
||||
def __enter__(self):
|
||||
pass
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class RAFT(nn.Module):
|
||||
def __init__(self, args):
|
||||
super(RAFT, self).__init__()
|
||||
self.args = args
|
||||
|
||||
if args.small:
|
||||
self.hidden_dim = hdim = 96
|
||||
self.context_dim = cdim = 64
|
||||
args.corr_levels = 4
|
||||
args.corr_radius = 3
|
||||
|
||||
else:
|
||||
self.hidden_dim = hdim = 128
|
||||
self.context_dim = cdim = 128
|
||||
args.corr_levels = 4
|
||||
args.corr_radius = 4
|
||||
|
||||
if 'dropout' not in args._get_kwargs():
|
||||
args.dropout = 0
|
||||
|
||||
if 'alternate_corr' not in args._get_kwargs():
|
||||
args.alternate_corr = False
|
||||
|
||||
# feature network, context network, and update block
|
||||
if args.small:
|
||||
self.fnet = SmallEncoder(output_dim=128, norm_fn='instance', dropout=args.dropout)
|
||||
self.cnet = SmallEncoder(output_dim=hdim+cdim, norm_fn='none', dropout=args.dropout)
|
||||
self.update_block = SmallUpdateBlock(self.args, hidden_dim=hdim)
|
||||
|
||||
else:
|
||||
self.fnet = BasicEncoder(output_dim=256, norm_fn='instance', dropout=args.dropout)
|
||||
self.cnet = BasicEncoder(output_dim=hdim+cdim, norm_fn='batch', dropout=args.dropout)
|
||||
self.update_block = BasicUpdateBlock(self.args, hidden_dim=hdim)
|
||||
|
||||
|
||||
def freeze_bn(self):
|
||||
for m in self.modules():
|
||||
if isinstance(m, nn.BatchNorm2d):
|
||||
m.eval()
|
||||
|
||||
def initialize_flow(self, img):
|
||||
""" Flow is represented as difference between two coordinate grids flow = coords1 - coords0"""
|
||||
N, C, H, W = img.shape
|
||||
coords0 = coords_grid(N, H//8, W//8).to(img.device)
|
||||
coords1 = coords_grid(N, H//8, W//8).to(img.device)
|
||||
|
||||
# optical flow computed as difference: flow = coords1 - coords0
|
||||
return coords0, coords1
|
||||
|
||||
def upsample_flow(self, flow, mask):
|
||||
""" Upsample flow field [H/8, W/8, 2] -> [H, W, 2] using convex combination """
|
||||
N, _, H, W = flow.shape
|
||||
mask = mask.view(N, 1, 9, 8, 8, H, W)
|
||||
mask = torch.softmax(mask, dim=2)
|
||||
|
||||
up_flow = F.unfold(8 * flow, [3,3], padding=1)
|
||||
up_flow = up_flow.view(N, 2, 9, 1, 1, H, W)
|
||||
|
||||
up_flow = torch.sum(mask * up_flow, dim=2)
|
||||
up_flow = up_flow.permute(0, 1, 4, 2, 5, 3)
|
||||
return up_flow.reshape(N, 2, 8*H, 8*W)
|
||||
|
||||
|
||||
def forward(self, image1, image2, iters=12, flow_init=None, test_mode=True):
|
||||
""" Estimate optical flow between pair of frames """
|
||||
|
||||
# image1 = 2 * (image1 / 255.0) - 1.0
|
||||
# image2 = 2 * (image2 / 255.0) - 1.0
|
||||
|
||||
image1 = image1.contiguous()
|
||||
image2 = image2.contiguous()
|
||||
|
||||
hdim = self.hidden_dim
|
||||
cdim = self.context_dim
|
||||
|
||||
# run the feature network
|
||||
with autocast(enabled=self.args.mixed_precision):
|
||||
fmap1, fmap2 = self.fnet([image1, image2])
|
||||
|
||||
fmap1 = fmap1.float()
|
||||
fmap2 = fmap2.float()
|
||||
|
||||
if self.args.alternate_corr:
|
||||
corr_fn = AlternateCorrBlock(fmap1, fmap2, radius=self.args.corr_radius)
|
||||
else:
|
||||
corr_fn = CorrBlock(fmap1, fmap2, radius=self.args.corr_radius)
|
||||
|
||||
# run the context network
|
||||
with autocast(enabled=self.args.mixed_precision):
|
||||
cnet = self.cnet(image1)
|
||||
net, inp = torch.split(cnet, [hdim, cdim], dim=1)
|
||||
net = torch.tanh(net)
|
||||
inp = torch.relu(inp)
|
||||
|
||||
coords0, coords1 = self.initialize_flow(image1)
|
||||
|
||||
if flow_init is not None:
|
||||
coords1 = coords1 + flow_init
|
||||
|
||||
flow_predictions = []
|
||||
for itr in range(iters):
|
||||
coords1 = coords1.detach()
|
||||
corr = corr_fn(coords1) # index correlation volume
|
||||
|
||||
flow = coords1 - coords0
|
||||
with autocast(enabled=self.args.mixed_precision):
|
||||
net, up_mask, delta_flow = self.update_block(net, inp, corr, flow)
|
||||
|
||||
# F(t+1) = F(t) + \Delta(t)
|
||||
coords1 = coords1 + delta_flow
|
||||
|
||||
# upsample predictions
|
||||
if up_mask is None:
|
||||
flow_up = upflow8(coords1 - coords0)
|
||||
else:
|
||||
flow_up = self.upsample_flow(coords1 - coords0, up_mask)
|
||||
|
||||
flow_predictions.append(flow_up)
|
||||
|
||||
if test_mode:
|
||||
return coords1 - coords0, flow_up
|
||||
|
||||
return flow_predictions
|
||||
@@ -0,0 +1,139 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class FlowHead(nn.Module):
|
||||
def __init__(self, input_dim=128, hidden_dim=256):
|
||||
super(FlowHead, self).__init__()
|
||||
self.conv1 = nn.Conv2d(input_dim, hidden_dim, 3, padding=1)
|
||||
self.conv2 = nn.Conv2d(hidden_dim, 2, 3, padding=1)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
return self.conv2(self.relu(self.conv1(x)))
|
||||
|
||||
class ConvGRU(nn.Module):
|
||||
def __init__(self, hidden_dim=128, input_dim=192+128):
|
||||
super(ConvGRU, self).__init__()
|
||||
self.convz = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
|
||||
self.convr = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
|
||||
self.convq = nn.Conv2d(hidden_dim+input_dim, hidden_dim, 3, padding=1)
|
||||
|
||||
def forward(self, h, x):
|
||||
hx = torch.cat([h, x], dim=1)
|
||||
|
||||
z = torch.sigmoid(self.convz(hx))
|
||||
r = torch.sigmoid(self.convr(hx))
|
||||
q = torch.tanh(self.convq(torch.cat([r*h, x], dim=1)))
|
||||
|
||||
h = (1-z) * h + z * q
|
||||
return h
|
||||
|
||||
class SepConvGRU(nn.Module):
|
||||
def __init__(self, hidden_dim=128, input_dim=192+128):
|
||||
super(SepConvGRU, self).__init__()
|
||||
self.convz1 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (1,5), padding=(0,2))
|
||||
self.convr1 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (1,5), padding=(0,2))
|
||||
self.convq1 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (1,5), padding=(0,2))
|
||||
|
||||
self.convz2 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (5,1), padding=(2,0))
|
||||
self.convr2 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (5,1), padding=(2,0))
|
||||
self.convq2 = nn.Conv2d(hidden_dim+input_dim, hidden_dim, (5,1), padding=(2,0))
|
||||
|
||||
|
||||
def forward(self, h, x):
|
||||
# horizontal
|
||||
hx = torch.cat([h, x], dim=1)
|
||||
z = torch.sigmoid(self.convz1(hx))
|
||||
r = torch.sigmoid(self.convr1(hx))
|
||||
q = torch.tanh(self.convq1(torch.cat([r*h, x], dim=1)))
|
||||
h = (1-z) * h + z * q
|
||||
|
||||
# vertical
|
||||
hx = torch.cat([h, x], dim=1)
|
||||
z = torch.sigmoid(self.convz2(hx))
|
||||
r = torch.sigmoid(self.convr2(hx))
|
||||
q = torch.tanh(self.convq2(torch.cat([r*h, x], dim=1)))
|
||||
h = (1-z) * h + z * q
|
||||
|
||||
return h
|
||||
|
||||
class SmallMotionEncoder(nn.Module):
|
||||
def __init__(self, args):
|
||||
super(SmallMotionEncoder, self).__init__()
|
||||
cor_planes = args.corr_levels * (2*args.corr_radius + 1)**2
|
||||
self.convc1 = nn.Conv2d(cor_planes, 96, 1, padding=0)
|
||||
self.convf1 = nn.Conv2d(2, 64, 7, padding=3)
|
||||
self.convf2 = nn.Conv2d(64, 32, 3, padding=1)
|
||||
self.conv = nn.Conv2d(128, 80, 3, padding=1)
|
||||
|
||||
def forward(self, flow, corr):
|
||||
cor = F.relu(self.convc1(corr))
|
||||
flo = F.relu(self.convf1(flow))
|
||||
flo = F.relu(self.convf2(flo))
|
||||
cor_flo = torch.cat([cor, flo], dim=1)
|
||||
out = F.relu(self.conv(cor_flo))
|
||||
return torch.cat([out, flow], dim=1)
|
||||
|
||||
class BasicMotionEncoder(nn.Module):
|
||||
def __init__(self, args):
|
||||
super(BasicMotionEncoder, self).__init__()
|
||||
cor_planes = args.corr_levels * (2*args.corr_radius + 1)**2
|
||||
self.convc1 = nn.Conv2d(cor_planes, 256, 1, padding=0)
|
||||
self.convc2 = nn.Conv2d(256, 192, 3, padding=1)
|
||||
self.convf1 = nn.Conv2d(2, 128, 7, padding=3)
|
||||
self.convf2 = nn.Conv2d(128, 64, 3, padding=1)
|
||||
self.conv = nn.Conv2d(64+192, 128-2, 3, padding=1)
|
||||
|
||||
def forward(self, flow, corr):
|
||||
cor = F.relu(self.convc1(corr))
|
||||
cor = F.relu(self.convc2(cor))
|
||||
flo = F.relu(self.convf1(flow))
|
||||
flo = F.relu(self.convf2(flo))
|
||||
|
||||
cor_flo = torch.cat([cor, flo], dim=1)
|
||||
out = F.relu(self.conv(cor_flo))
|
||||
return torch.cat([out, flow], dim=1)
|
||||
|
||||
class SmallUpdateBlock(nn.Module):
|
||||
def __init__(self, args, hidden_dim=96):
|
||||
super(SmallUpdateBlock, self).__init__()
|
||||
self.encoder = SmallMotionEncoder(args)
|
||||
self.gru = ConvGRU(hidden_dim=hidden_dim, input_dim=82+64)
|
||||
self.flow_head = FlowHead(hidden_dim, hidden_dim=128)
|
||||
|
||||
def forward(self, net, inp, corr, flow):
|
||||
motion_features = self.encoder(flow, corr)
|
||||
inp = torch.cat([inp, motion_features], dim=1)
|
||||
net = self.gru(net, inp)
|
||||
delta_flow = self.flow_head(net)
|
||||
|
||||
return net, None, delta_flow
|
||||
|
||||
class BasicUpdateBlock(nn.Module):
|
||||
def __init__(self, args, hidden_dim=128, input_dim=128):
|
||||
super(BasicUpdateBlock, self).__init__()
|
||||
self.args = args
|
||||
self.encoder = BasicMotionEncoder(args)
|
||||
self.gru = SepConvGRU(hidden_dim=hidden_dim, input_dim=128+hidden_dim)
|
||||
self.flow_head = FlowHead(hidden_dim, hidden_dim=256)
|
||||
|
||||
self.mask = nn.Sequential(
|
||||
nn.Conv2d(128, 256, 3, padding=1),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Conv2d(256, 64*9, 1, padding=0))
|
||||
|
||||
def forward(self, net, inp, corr, flow, upsample=True):
|
||||
motion_features = self.encoder(flow, corr)
|
||||
inp = torch.cat([inp, motion_features], dim=1)
|
||||
|
||||
net = self.gru(net, inp)
|
||||
delta_flow = self.flow_head(net)
|
||||
|
||||
# scale mask to balence gradients
|
||||
mask = .25 * self.mask(net)
|
||||
return net, mask, delta_flow
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from .flow_viz import flow_to_image
|
||||
from .frame_utils import writeFlow
|
||||
@@ -0,0 +1,246 @@
|
||||
import numpy as np
|
||||
import random
|
||||
import math
|
||||
from PIL import Image
|
||||
|
||||
import cv2
|
||||
cv2.setNumThreads(0)
|
||||
cv2.ocl.setUseOpenCL(False)
|
||||
|
||||
import torch
|
||||
from torchvision.transforms import ColorJitter
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class FlowAugmentor:
|
||||
def __init__(self, crop_size, min_scale=-0.2, max_scale=0.5, do_flip=True):
|
||||
|
||||
# spatial augmentation params
|
||||
self.crop_size = crop_size
|
||||
self.min_scale = min_scale
|
||||
self.max_scale = max_scale
|
||||
self.spatial_aug_prob = 0.8
|
||||
self.stretch_prob = 0.8
|
||||
self.max_stretch = 0.2
|
||||
|
||||
# flip augmentation params
|
||||
self.do_flip = do_flip
|
||||
self.h_flip_prob = 0.5
|
||||
self.v_flip_prob = 0.1
|
||||
|
||||
# photometric augmentation params
|
||||
self.photo_aug = ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4, hue=0.5/3.14)
|
||||
self.asymmetric_color_aug_prob = 0.2
|
||||
self.eraser_aug_prob = 0.5
|
||||
|
||||
def color_transform(self, img1, img2):
|
||||
""" Photometric augmentation """
|
||||
|
||||
# asymmetric
|
||||
if np.random.rand() < self.asymmetric_color_aug_prob:
|
||||
img1 = np.array(self.photo_aug(Image.fromarray(img1)), dtype=np.uint8)
|
||||
img2 = np.array(self.photo_aug(Image.fromarray(img2)), dtype=np.uint8)
|
||||
|
||||
# symmetric
|
||||
else:
|
||||
image_stack = np.concatenate([img1, img2], axis=0)
|
||||
image_stack = np.array(self.photo_aug(Image.fromarray(image_stack)), dtype=np.uint8)
|
||||
img1, img2 = np.split(image_stack, 2, axis=0)
|
||||
|
||||
return img1, img2
|
||||
|
||||
def eraser_transform(self, img1, img2, bounds=[50, 100]):
|
||||
""" Occlusion augmentation """
|
||||
|
||||
ht, wd = img1.shape[:2]
|
||||
if np.random.rand() < self.eraser_aug_prob:
|
||||
mean_color = np.mean(img2.reshape(-1, 3), axis=0)
|
||||
for _ in range(np.random.randint(1, 3)):
|
||||
x0 = np.random.randint(0, wd)
|
||||
y0 = np.random.randint(0, ht)
|
||||
dx = np.random.randint(bounds[0], bounds[1])
|
||||
dy = np.random.randint(bounds[0], bounds[1])
|
||||
img2[y0:y0+dy, x0:x0+dx, :] = mean_color
|
||||
|
||||
return img1, img2
|
||||
|
||||
def spatial_transform(self, img1, img2, flow):
|
||||
# randomly sample scale
|
||||
ht, wd = img1.shape[:2]
|
||||
min_scale = np.maximum(
|
||||
(self.crop_size[0] + 8) / float(ht),
|
||||
(self.crop_size[1] + 8) / float(wd))
|
||||
|
||||
scale = 2 ** np.random.uniform(self.min_scale, self.max_scale)
|
||||
scale_x = scale
|
||||
scale_y = scale
|
||||
if np.random.rand() < self.stretch_prob:
|
||||
scale_x *= 2 ** np.random.uniform(-self.max_stretch, self.max_stretch)
|
||||
scale_y *= 2 ** np.random.uniform(-self.max_stretch, self.max_stretch)
|
||||
|
||||
scale_x = np.clip(scale_x, min_scale, None)
|
||||
scale_y = np.clip(scale_y, min_scale, None)
|
||||
|
||||
if np.random.rand() < self.spatial_aug_prob:
|
||||
# rescale the images
|
||||
img1 = cv2.resize(img1, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
|
||||
img2 = cv2.resize(img2, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
|
||||
flow = cv2.resize(flow, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
|
||||
flow = flow * [scale_x, scale_y]
|
||||
|
||||
if self.do_flip:
|
||||
if np.random.rand() < self.h_flip_prob: # h-flip
|
||||
img1 = img1[:, ::-1]
|
||||
img2 = img2[:, ::-1]
|
||||
flow = flow[:, ::-1] * [-1.0, 1.0]
|
||||
|
||||
if np.random.rand() < self.v_flip_prob: # v-flip
|
||||
img1 = img1[::-1, :]
|
||||
img2 = img2[::-1, :]
|
||||
flow = flow[::-1, :] * [1.0, -1.0]
|
||||
|
||||
y0 = np.random.randint(0, img1.shape[0] - self.crop_size[0])
|
||||
x0 = np.random.randint(0, img1.shape[1] - self.crop_size[1])
|
||||
|
||||
img1 = img1[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
img2 = img2[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
flow = flow[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
|
||||
return img1, img2, flow
|
||||
|
||||
def __call__(self, img1, img2, flow):
|
||||
img1, img2 = self.color_transform(img1, img2)
|
||||
img1, img2 = self.eraser_transform(img1, img2)
|
||||
img1, img2, flow = self.spatial_transform(img1, img2, flow)
|
||||
|
||||
img1 = np.ascontiguousarray(img1)
|
||||
img2 = np.ascontiguousarray(img2)
|
||||
flow = np.ascontiguousarray(flow)
|
||||
|
||||
return img1, img2, flow
|
||||
|
||||
class SparseFlowAugmentor:
|
||||
def __init__(self, crop_size, min_scale=-0.2, max_scale=0.5, do_flip=False):
|
||||
# spatial augmentation params
|
||||
self.crop_size = crop_size
|
||||
self.min_scale = min_scale
|
||||
self.max_scale = max_scale
|
||||
self.spatial_aug_prob = 0.8
|
||||
self.stretch_prob = 0.8
|
||||
self.max_stretch = 0.2
|
||||
|
||||
# flip augmentation params
|
||||
self.do_flip = do_flip
|
||||
self.h_flip_prob = 0.5
|
||||
self.v_flip_prob = 0.1
|
||||
|
||||
# photometric augmentation params
|
||||
self.photo_aug = ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.3/3.14)
|
||||
self.asymmetric_color_aug_prob = 0.2
|
||||
self.eraser_aug_prob = 0.5
|
||||
|
||||
def color_transform(self, img1, img2):
|
||||
image_stack = np.concatenate([img1, img2], axis=0)
|
||||
image_stack = np.array(self.photo_aug(Image.fromarray(image_stack)), dtype=np.uint8)
|
||||
img1, img2 = np.split(image_stack, 2, axis=0)
|
||||
return img1, img2
|
||||
|
||||
def eraser_transform(self, img1, img2):
|
||||
ht, wd = img1.shape[:2]
|
||||
if np.random.rand() < self.eraser_aug_prob:
|
||||
mean_color = np.mean(img2.reshape(-1, 3), axis=0)
|
||||
for _ in range(np.random.randint(1, 3)):
|
||||
x0 = np.random.randint(0, wd)
|
||||
y0 = np.random.randint(0, ht)
|
||||
dx = np.random.randint(50, 100)
|
||||
dy = np.random.randint(50, 100)
|
||||
img2[y0:y0+dy, x0:x0+dx, :] = mean_color
|
||||
|
||||
return img1, img2
|
||||
|
||||
def resize_sparse_flow_map(self, flow, valid, fx=1.0, fy=1.0):
|
||||
ht, wd = flow.shape[:2]
|
||||
coords = np.meshgrid(np.arange(wd), np.arange(ht))
|
||||
coords = np.stack(coords, axis=-1)
|
||||
|
||||
coords = coords.reshape(-1, 2).astype(np.float32)
|
||||
flow = flow.reshape(-1, 2).astype(np.float32)
|
||||
valid = valid.reshape(-1).astype(np.float32)
|
||||
|
||||
coords0 = coords[valid>=1]
|
||||
flow0 = flow[valid>=1]
|
||||
|
||||
ht1 = int(round(ht * fy))
|
||||
wd1 = int(round(wd * fx))
|
||||
|
||||
coords1 = coords0 * [fx, fy]
|
||||
flow1 = flow0 * [fx, fy]
|
||||
|
||||
xx = np.round(coords1[:,0]).astype(np.int32)
|
||||
yy = np.round(coords1[:,1]).astype(np.int32)
|
||||
|
||||
v = (xx > 0) & (xx < wd1) & (yy > 0) & (yy < ht1)
|
||||
xx = xx[v]
|
||||
yy = yy[v]
|
||||
flow1 = flow1[v]
|
||||
|
||||
flow_img = np.zeros([ht1, wd1, 2], dtype=np.float32)
|
||||
valid_img = np.zeros([ht1, wd1], dtype=np.int32)
|
||||
|
||||
flow_img[yy, xx] = flow1
|
||||
valid_img[yy, xx] = 1
|
||||
|
||||
return flow_img, valid_img
|
||||
|
||||
def spatial_transform(self, img1, img2, flow, valid):
|
||||
# randomly sample scale
|
||||
|
||||
ht, wd = img1.shape[:2]
|
||||
min_scale = np.maximum(
|
||||
(self.crop_size[0] + 1) / float(ht),
|
||||
(self.crop_size[1] + 1) / float(wd))
|
||||
|
||||
scale = 2 ** np.random.uniform(self.min_scale, self.max_scale)
|
||||
scale_x = np.clip(scale, min_scale, None)
|
||||
scale_y = np.clip(scale, min_scale, None)
|
||||
|
||||
if np.random.rand() < self.spatial_aug_prob:
|
||||
# rescale the images
|
||||
img1 = cv2.resize(img1, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
|
||||
img2 = cv2.resize(img2, None, fx=scale_x, fy=scale_y, interpolation=cv2.INTER_LINEAR)
|
||||
flow, valid = self.resize_sparse_flow_map(flow, valid, fx=scale_x, fy=scale_y)
|
||||
|
||||
if self.do_flip:
|
||||
if np.random.rand() < 0.5: # h-flip
|
||||
img1 = img1[:, ::-1]
|
||||
img2 = img2[:, ::-1]
|
||||
flow = flow[:, ::-1] * [-1.0, 1.0]
|
||||
valid = valid[:, ::-1]
|
||||
|
||||
margin_y = 20
|
||||
margin_x = 50
|
||||
|
||||
y0 = np.random.randint(0, img1.shape[0] - self.crop_size[0] + margin_y)
|
||||
x0 = np.random.randint(-margin_x, img1.shape[1] - self.crop_size[1] + margin_x)
|
||||
|
||||
y0 = np.clip(y0, 0, img1.shape[0] - self.crop_size[0])
|
||||
x0 = np.clip(x0, 0, img1.shape[1] - self.crop_size[1])
|
||||
|
||||
img1 = img1[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
img2 = img2[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
flow = flow[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
valid = valid[y0:y0+self.crop_size[0], x0:x0+self.crop_size[1]]
|
||||
return img1, img2, flow, valid
|
||||
|
||||
|
||||
def __call__(self, img1, img2, flow, valid):
|
||||
img1, img2 = self.color_transform(img1, img2)
|
||||
img1, img2 = self.eraser_transform(img1, img2)
|
||||
img1, img2, flow, valid = self.spatial_transform(img1, img2, flow, valid)
|
||||
|
||||
img1 = np.ascontiguousarray(img1)
|
||||
img2 = np.ascontiguousarray(img2)
|
||||
flow = np.ascontiguousarray(flow)
|
||||
valid = np.ascontiguousarray(valid)
|
||||
|
||||
return img1, img2, flow, valid
|
||||
@@ -0,0 +1,132 @@
|
||||
# Flow visualization code used from https://github.com/tomrunia/OpticalFlow_Visualization
|
||||
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2018 Tom Runia
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to conditions.
|
||||
#
|
||||
# Author: Tom Runia
|
||||
# Date Created: 2018-08-03
|
||||
|
||||
import numpy as np
|
||||
|
||||
def make_colorwheel():
|
||||
"""
|
||||
Generates a color wheel for optical flow visualization as presented in:
|
||||
Baker et al. "A Database and Evaluation Methodology for Optical Flow" (ICCV, 2007)
|
||||
URL: http://vision.middlebury.edu/flow/flowEval-iccv07.pdf
|
||||
|
||||
Code follows the original C++ source code of Daniel Scharstein.
|
||||
Code follows the the Matlab source code of Deqing Sun.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Color wheel
|
||||
"""
|
||||
|
||||
RY = 15
|
||||
YG = 6
|
||||
GC = 4
|
||||
CB = 11
|
||||
BM = 13
|
||||
MR = 6
|
||||
|
||||
ncols = RY + YG + GC + CB + BM + MR
|
||||
colorwheel = np.zeros((ncols, 3))
|
||||
col = 0
|
||||
|
||||
# RY
|
||||
colorwheel[0:RY, 0] = 255
|
||||
colorwheel[0:RY, 1] = np.floor(255*np.arange(0,RY)/RY)
|
||||
col = col+RY
|
||||
# YG
|
||||
colorwheel[col:col+YG, 0] = 255 - np.floor(255*np.arange(0,YG)/YG)
|
||||
colorwheel[col:col+YG, 1] = 255
|
||||
col = col+YG
|
||||
# GC
|
||||
colorwheel[col:col+GC, 1] = 255
|
||||
colorwheel[col:col+GC, 2] = np.floor(255*np.arange(0,GC)/GC)
|
||||
col = col+GC
|
||||
# CB
|
||||
colorwheel[col:col+CB, 1] = 255 - np.floor(255*np.arange(CB)/CB)
|
||||
colorwheel[col:col+CB, 2] = 255
|
||||
col = col+CB
|
||||
# BM
|
||||
colorwheel[col:col+BM, 2] = 255
|
||||
colorwheel[col:col+BM, 0] = np.floor(255*np.arange(0,BM)/BM)
|
||||
col = col+BM
|
||||
# MR
|
||||
colorwheel[col:col+MR, 2] = 255 - np.floor(255*np.arange(MR)/MR)
|
||||
colorwheel[col:col+MR, 0] = 255
|
||||
return colorwheel
|
||||
|
||||
|
||||
def flow_uv_to_colors(u, v, convert_to_bgr=False):
|
||||
"""
|
||||
Applies the flow color wheel to (possibly clipped) flow components u and v.
|
||||
|
||||
According to the C++ source code of Daniel Scharstein
|
||||
According to the Matlab source code of Deqing Sun
|
||||
|
||||
Args:
|
||||
u (np.ndarray): Input horizontal flow of shape [H,W]
|
||||
v (np.ndarray): Input vertical flow of shape [H,W]
|
||||
convert_to_bgr (bool, optional): Convert output image to BGR. Defaults to False.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Flow visualization image of shape [H,W,3]
|
||||
"""
|
||||
flow_image = np.zeros((u.shape[0], u.shape[1], 3), np.uint8)
|
||||
colorwheel = make_colorwheel() # shape [55x3]
|
||||
ncols = colorwheel.shape[0]
|
||||
rad = np.sqrt(np.square(u) + np.square(v))
|
||||
a = np.arctan2(-v, -u)/np.pi
|
||||
fk = (a+1) / 2*(ncols-1)
|
||||
k0 = np.floor(fk).astype(np.int32)
|
||||
k1 = k0 + 1
|
||||
k1[k1 == ncols] = 0
|
||||
f = fk - k0
|
||||
for i in range(colorwheel.shape[1]):
|
||||
tmp = colorwheel[:,i]
|
||||
col0 = tmp[k0] / 255.0
|
||||
col1 = tmp[k1] / 255.0
|
||||
col = (1-f)*col0 + f*col1
|
||||
idx = (rad <= 1)
|
||||
col[idx] = 1 - rad[idx] * (1-col[idx])
|
||||
col[~idx] = col[~idx] * 0.75 # out of range
|
||||
# Note the 2-i => BGR instead of RGB
|
||||
ch_idx = 2-i if convert_to_bgr else i
|
||||
flow_image[:,:,ch_idx] = np.floor(255 * col)
|
||||
return flow_image
|
||||
|
||||
|
||||
def flow_to_image(flow_uv, clip_flow=None, convert_to_bgr=False):
|
||||
"""
|
||||
Expects a two dimensional flow image of shape.
|
||||
|
||||
Args:
|
||||
flow_uv (np.ndarray): Flow UV image of shape [H,W,2]
|
||||
clip_flow (float, optional): Clip maximum of flow values. Defaults to None.
|
||||
convert_to_bgr (bool, optional): Convert output image to BGR. Defaults to False.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Flow visualization image of shape [H,W,3]
|
||||
"""
|
||||
assert flow_uv.ndim == 3, 'input flow must have three dimensions'
|
||||
assert flow_uv.shape[2] == 2, 'input flow must have shape [H,W,2]'
|
||||
if clip_flow is not None:
|
||||
flow_uv = np.clip(flow_uv, 0, clip_flow)
|
||||
u = flow_uv[:,:,0]
|
||||
v = flow_uv[:,:,1]
|
||||
rad = np.sqrt(np.square(u) + np.square(v))
|
||||
rad_max = np.max(rad)
|
||||
epsilon = 1e-5
|
||||
u = u / (rad_max + epsilon)
|
||||
v = v / (rad_max + epsilon)
|
||||
return flow_uv_to_colors(u, v, convert_to_bgr)
|
||||
@@ -0,0 +1,118 @@
|
||||
# Flow visualization code adapted from https://github.com/tomrunia/OpticalFlow_Visualization
|
||||
import torch
|
||||
torch.pi = torch.acos(torch.zeros(1)).item() * 2 # which is 3.1415927410125732
|
||||
|
||||
@torch.no_grad()
|
||||
def flow_to_image(flow: torch.Tensor) -> torch.Tensor:
|
||||
|
||||
"""
|
||||
Converts a flow to an RGB image.
|
||||
|
||||
Args:
|
||||
flow (Tensor): Flow of shape (N, 2, H, W) or (2, H, W) and dtype torch.float.
|
||||
|
||||
Returns:
|
||||
img (Tensor): Image Tensor of dtype uint8 where each color corresponds
|
||||
to a given flow direction. Shape is (N, 3, H, W) or (3, H, W) depending on the input.
|
||||
"""
|
||||
|
||||
if flow.dtype != torch.float:
|
||||
raise ValueError(f"Flow should be of dtype torch.float, got {flow.dtype}.")
|
||||
|
||||
orig_shape = flow.shape
|
||||
if flow.ndim == 3:
|
||||
flow = flow[None] # Add batch dim
|
||||
|
||||
if flow.ndim != 4 or flow.shape[1] != 2:
|
||||
raise ValueError(f"Input flow should have shape (2, H, W) or (N, 2, H, W), got {orig_shape}.")
|
||||
|
||||
max_norm = torch.sum(flow**2, dim=1).sqrt().max()
|
||||
epsilon = torch.finfo((flow).dtype).eps
|
||||
normalized_flow = flow / (max_norm + epsilon)
|
||||
img = _normalized_flow_to_image(normalized_flow)
|
||||
|
||||
if len(orig_shape) == 3:
|
||||
img = img[0] # Remove batch dim
|
||||
return img
|
||||
|
||||
@torch.no_grad()
|
||||
def _normalized_flow_to_image(normalized_flow: torch.Tensor) -> torch.Tensor:
|
||||
|
||||
"""
|
||||
Converts a batch of normalized flow to an RGB image.
|
||||
|
||||
Args:
|
||||
normalized_flow (torch.Tensor): Normalized flow tensor of shape (N, 2, H, W)
|
||||
Returns:
|
||||
img (Tensor(N, 3, H, W)): Flow visualization image of dtype uint8.
|
||||
"""
|
||||
|
||||
N, _, H, W = normalized_flow.shape
|
||||
device = normalized_flow.device
|
||||
flow_image = torch.zeros((N, 3, H, W), dtype=torch.uint8, device=device)
|
||||
colorwheel = _make_colorwheel().to(device) # shape [55x3]
|
||||
num_cols = colorwheel.shape[0]
|
||||
norm = torch.sum(normalized_flow**2, dim=1).sqrt()
|
||||
a = torch.atan2(-normalized_flow[:, 1, :, :], -normalized_flow[:, 0, :, :]) / torch.pi
|
||||
fk = (a + 1) / 2 * (num_cols - 1)
|
||||
k0 = torch.floor(fk).to(torch.long)
|
||||
k1 = k0 + 1
|
||||
k1[k1 == num_cols] = 0
|
||||
f = fk - k0
|
||||
|
||||
for c in range(colorwheel.shape[1]):
|
||||
tmp = colorwheel[:, c]
|
||||
col0 = tmp[k0] / 255.0
|
||||
col1 = tmp[k1] / 255.0
|
||||
col = (1 - f) * col0 + f * col1
|
||||
col = 1 - norm * (1 - col)
|
||||
flow_image[:, c, :, :] = torch.floor(255. * col)
|
||||
return flow_image
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def _make_colorwheel() -> torch.Tensor:
|
||||
"""
|
||||
Generates a color wheel for optical flow visualization as presented in:
|
||||
Baker et al. "A Database and Evaluation Methodology for Optical Flow" (ICCV, 2007)
|
||||
URL: http://vision.middlebury.edu/flow/flowEval-iccv07.pdf.
|
||||
|
||||
Returns:
|
||||
colorwheel (Tensor[55, 3]): Colorwheel Tensor.
|
||||
"""
|
||||
|
||||
RY = 15
|
||||
YG = 6
|
||||
GC = 4
|
||||
CB = 11
|
||||
BM = 13
|
||||
MR = 6
|
||||
|
||||
ncols = RY + YG + GC + CB + BM + MR
|
||||
colorwheel = torch.zeros((ncols, 3))
|
||||
col = 0
|
||||
|
||||
# RY
|
||||
colorwheel[0:RY, 0] = 255
|
||||
colorwheel[0:RY, 1] = torch.floor(255. * torch.arange(0., RY) / RY)
|
||||
col = col + RY
|
||||
# YG
|
||||
colorwheel[col : col + YG, 0] = 255 - torch.floor(255. * torch.arange(0., YG) / YG)
|
||||
colorwheel[col : col + YG, 1] = 255
|
||||
col = col + YG
|
||||
# GC
|
||||
colorwheel[col : col + GC, 1] = 255
|
||||
colorwheel[col : col + GC, 2] = torch.floor(255. * torch.arange(0., GC) / GC)
|
||||
col = col + GC
|
||||
# CB
|
||||
colorwheel[col : col + CB, 1] = 255 - torch.floor(255. * torch.arange(CB) / CB)
|
||||
colorwheel[col : col + CB, 2] = 255
|
||||
col = col + CB
|
||||
# BM
|
||||
colorwheel[col : col + BM, 2] = 255
|
||||
colorwheel[col : col + BM, 0] = torch.floor(255. * torch.arange(0., BM) / BM)
|
||||
col = col + BM
|
||||
# MR
|
||||
colorwheel[col : col + MR, 2] = 255 - torch.floor(255. * torch.arange(MR) / MR)
|
||||
colorwheel[col : col + MR, 0] = 255
|
||||
return colorwheel
|
||||
@@ -0,0 +1,137 @@
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from os.path import *
|
||||
import re
|
||||
|
||||
import cv2
|
||||
cv2.setNumThreads(0)
|
||||
cv2.ocl.setUseOpenCL(False)
|
||||
|
||||
TAG_CHAR = np.array([202021.25], np.float32)
|
||||
|
||||
def readFlow(fn):
|
||||
""" Read .flo file in Middlebury format"""
|
||||
# Code adapted from:
|
||||
# http://stackoverflow.com/questions/28013200/reading-middlebury-flow-files-with-python-bytes-array-numpy
|
||||
|
||||
# WARNING: this will work on little-endian architectures (eg Intel x86) only!
|
||||
# print 'fn = %s'%(fn)
|
||||
with open(fn, 'rb') as f:
|
||||
magic = np.fromfile(f, np.float32, count=1)
|
||||
if 202021.25 != magic:
|
||||
print('Magic number incorrect. Invalid .flo file')
|
||||
return None
|
||||
else:
|
||||
w = np.fromfile(f, np.int32, count=1)
|
||||
h = np.fromfile(f, np.int32, count=1)
|
||||
# print 'Reading %d x %d flo file\n' % (w, h)
|
||||
data = np.fromfile(f, np.float32, count=2*int(w)*int(h))
|
||||
# Reshape data into 3D array (columns, rows, bands)
|
||||
# The reshape here is for visualization, the original code is (w,h,2)
|
||||
return np.resize(data, (int(h), int(w), 2))
|
||||
|
||||
def readPFM(file):
|
||||
file = open(file, 'rb')
|
||||
|
||||
color = None
|
||||
width = None
|
||||
height = None
|
||||
scale = None
|
||||
endian = None
|
||||
|
||||
header = file.readline().rstrip()
|
||||
if header == b'PF':
|
||||
color = True
|
||||
elif header == b'Pf':
|
||||
color = False
|
||||
else:
|
||||
raise Exception('Not a PFM file.')
|
||||
|
||||
dim_match = re.match(rb'^(\d+)\s(\d+)\s$', file.readline())
|
||||
if dim_match:
|
||||
width, height = map(int, dim_match.groups())
|
||||
else:
|
||||
raise Exception('Malformed PFM header.')
|
||||
|
||||
scale = float(file.readline().rstrip())
|
||||
if scale < 0: # little-endian
|
||||
endian = '<'
|
||||
scale = -scale
|
||||
else:
|
||||
endian = '>' # big-endian
|
||||
|
||||
data = np.fromfile(file, endian + 'f')
|
||||
shape = (height, width, 3) if color else (height, width)
|
||||
|
||||
data = np.reshape(data, shape)
|
||||
data = np.flipud(data)
|
||||
return data
|
||||
|
||||
def writeFlow(filename,uv,v=None):
|
||||
""" Write optical flow to file.
|
||||
|
||||
If v is None, uv is assumed to contain both u and v channels,
|
||||
stacked in depth.
|
||||
Original code by Deqing Sun, adapted from Daniel Scharstein.
|
||||
"""
|
||||
nBands = 2
|
||||
|
||||
if v is None:
|
||||
assert(uv.ndim == 3)
|
||||
assert(uv.shape[2] == 2)
|
||||
u = uv[:,:,0]
|
||||
v = uv[:,:,1]
|
||||
else:
|
||||
u = uv
|
||||
|
||||
assert(u.shape == v.shape)
|
||||
height,width = u.shape
|
||||
f = open(filename,'wb')
|
||||
# write the header
|
||||
f.write(TAG_CHAR)
|
||||
np.array(width).astype(np.int32).tofile(f)
|
||||
np.array(height).astype(np.int32).tofile(f)
|
||||
# arrange into matrix form
|
||||
tmp = np.zeros((height, width*nBands))
|
||||
tmp[:,np.arange(width)*2] = u
|
||||
tmp[:,np.arange(width)*2 + 1] = v
|
||||
tmp.astype(np.float32).tofile(f)
|
||||
f.close()
|
||||
|
||||
|
||||
def readFlowKITTI(filename):
|
||||
flow = cv2.imread(filename, cv2.IMREAD_ANYDEPTH|cv2.IMREAD_COLOR)
|
||||
flow = flow[:,:,::-1].astype(np.float32)
|
||||
flow, valid = flow[:, :, :2], flow[:, :, 2]
|
||||
flow = (flow - 2**15) / 64.0
|
||||
return flow, valid
|
||||
|
||||
def readDispKITTI(filename):
|
||||
disp = cv2.imread(filename, cv2.IMREAD_ANYDEPTH) / 256.0
|
||||
valid = disp > 0.0
|
||||
flow = np.stack([-disp, np.zeros_like(disp)], -1)
|
||||
return flow, valid
|
||||
|
||||
|
||||
def writeFlowKITTI(filename, uv):
|
||||
uv = 64.0 * uv + 2**15
|
||||
valid = np.ones([uv.shape[0], uv.shape[1], 1])
|
||||
uv = np.concatenate([uv, valid], axis=-1).astype(np.uint16)
|
||||
cv2.imwrite(filename, uv[..., ::-1])
|
||||
|
||||
|
||||
def read_gen(file_name, pil=False):
|
||||
ext = splitext(file_name)[-1]
|
||||
if ext == '.png' or ext == '.jpeg' or ext == '.ppm' or ext == '.jpg':
|
||||
return Image.open(file_name)
|
||||
elif ext == '.bin' or ext == '.raw':
|
||||
return np.load(file_name)
|
||||
elif ext == '.flo':
|
||||
return readFlow(file_name).astype(np.float32)
|
||||
elif ext == '.pfm':
|
||||
flow = readPFM(file_name).astype(np.float32)
|
||||
if len(flow.shape) == 2:
|
||||
return flow
|
||||
else:
|
||||
return flow[:, :, :-1]
|
||||
return []
|
||||
@@ -0,0 +1,82 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import numpy as np
|
||||
from scipy import interpolate
|
||||
|
||||
|
||||
class InputPadder:
|
||||
""" Pads images such that dimensions are divisible by 8 """
|
||||
def __init__(self, dims, mode='sintel'):
|
||||
self.ht, self.wd = dims[-2:]
|
||||
pad_ht = (((self.ht // 8) + 1) * 8 - self.ht) % 8
|
||||
pad_wd = (((self.wd // 8) + 1) * 8 - self.wd) % 8
|
||||
if mode == 'sintel':
|
||||
self._pad = [pad_wd//2, pad_wd - pad_wd//2, pad_ht//2, pad_ht - pad_ht//2]
|
||||
else:
|
||||
self._pad = [pad_wd//2, pad_wd - pad_wd//2, 0, pad_ht]
|
||||
|
||||
def pad(self, *inputs):
|
||||
return [F.pad(x, self._pad, mode='replicate') for x in inputs]
|
||||
|
||||
def unpad(self,x):
|
||||
ht, wd = x.shape[-2:]
|
||||
c = [self._pad[2], ht-self._pad[3], self._pad[0], wd-self._pad[1]]
|
||||
return x[..., c[0]:c[1], c[2]:c[3]]
|
||||
|
||||
def forward_interpolate(flow):
|
||||
flow = flow.detach().cpu().numpy()
|
||||
dx, dy = flow[0], flow[1]
|
||||
|
||||
ht, wd = dx.shape
|
||||
x0, y0 = np.meshgrid(np.arange(wd), np.arange(ht))
|
||||
|
||||
x1 = x0 + dx
|
||||
y1 = y0 + dy
|
||||
|
||||
x1 = x1.reshape(-1)
|
||||
y1 = y1.reshape(-1)
|
||||
dx = dx.reshape(-1)
|
||||
dy = dy.reshape(-1)
|
||||
|
||||
valid = (x1 > 0) & (x1 < wd) & (y1 > 0) & (y1 < ht)
|
||||
x1 = x1[valid]
|
||||
y1 = y1[valid]
|
||||
dx = dx[valid]
|
||||
dy = dy[valid]
|
||||
|
||||
flow_x = interpolate.griddata(
|
||||
(x1, y1), dx, (x0, y0), method='nearest', fill_value=0)
|
||||
|
||||
flow_y = interpolate.griddata(
|
||||
(x1, y1), dy, (x0, y0), method='nearest', fill_value=0)
|
||||
|
||||
flow = np.stack([flow_x, flow_y], axis=0)
|
||||
return torch.from_numpy(flow).float()
|
||||
|
||||
|
||||
def bilinear_sampler(img, coords, mode='bilinear', mask=False):
|
||||
""" Wrapper for grid_sample, uses pixel coordinates """
|
||||
H, W = img.shape[-2:]
|
||||
xgrid, ygrid = coords.split([1,1], dim=-1)
|
||||
xgrid = 2*xgrid/(W-1) - 1
|
||||
ygrid = 2*ygrid/(H-1) - 1
|
||||
|
||||
grid = torch.cat([xgrid, ygrid], dim=-1)
|
||||
img = F.grid_sample(img, grid, align_corners=True)
|
||||
|
||||
if mask:
|
||||
mask = (xgrid > -1) & (ygrid > -1) & (xgrid < 1) & (ygrid < 1)
|
||||
return img, mask.float()
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def coords_grid(batch, ht, wd):
|
||||
coords = torch.meshgrid(torch.arange(ht), torch.arange(wd))
|
||||
coords = torch.stack(coords[::-1], dim=0).float()
|
||||
return coords[None].repeat(batch, 1, 1, 1)
|
||||
|
||||
|
||||
def upflow8(flow, mode='bilinear'):
|
||||
new_size = (8 * flow.shape[2], 8 * flow.shape[3])
|
||||
return 8 * F.interpolate(flow, size=new_size, mode=mode, align_corners=True)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="logo">
|
||||
<a href="https://shangchenzhou.com/projects/ProPainter/">
|
||||
<img src="assets/propainter_logo1_glow.png" style="width:180px;">
|
||||
<img src="assets/propainter_logo1_glow.png" style="width: 180px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -21,14 +21,30 @@
|
||||
<div>
|
||||
<strong>ICCV 2023</strong>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 align="center">
|
||||
<a href="https://shangchenzhou.com/projects/ProPainter/" target='_blank'>[Project Page]</a> •
|
||||
<a href="#" target='_blank'>[arXiv (coming soon)]</a> •
|
||||
<a href="https://youtu.be/Cc89WF-2zz0" target='_blank'>[Demo Video]</a>
|
||||
<a href="https://shangchenzhou.com/projects/ProPainter" target='_blank'>
|
||||
<img src="https://img.shields.io/badge/🐳-Project%20Page-blue">
|
||||
</a>
|
||||
<a href="https://arxiv.org/abs/2309.03897" target='_blank'>
|
||||
<img src="https://img.shields.io/badge/arXiv-2309.03897-b31b1b.svg">
|
||||
</a>
|
||||
<a href="https://youtu.be/92EHfgCO5-Q" target='_blank'>
|
||||
<img src="https://img.shields.io/badge/Demo%20Video-%23FF0000.svg?logo=YouTube&logoColor=white">
|
||||
</a>
|
||||
<a href="https://huggingface.co/spaces/sczhou/ProPainter" target='_blank'>
|
||||
<img src="https://img.shields.io/badge/Demo-%F0%9F%A4%97%20Hugging%20Face-blue">
|
||||
</a>
|
||||
<a href="https://openxlab.org.cn/apps/detail/ShangchenZhou/ProPainter" target='_blank'>
|
||||
<img src="https://img.shields.io/badge/Demo-%F0%9F%91%A8%E2%80%8D%F0%9F%8E%A8%20OpenXLab-blue">
|
||||
</a>
|
||||
<img src="https://api.infinitescript.com/badgen/count?name=sczhou/ProPainter">
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
⭐ If ProPainter is helpful to your projects, please help star this repo. Thanks! 🤗
|
||||
|
||||
:open_book: For more visual results, go checkout our <a href="https://shangchenzhou.com/projects/ProPainter/" target="_blank">project page</a>
|
||||
|
||||
|
||||
@@ -38,13 +54,22 @@
|
||||
|
||||
|
||||
## Update
|
||||
|
||||
<!-- - **2023.09.02**: Our code and model are publicly available. :whale: -->
|
||||
- **2023.11.09**: Integrated to :man_artist: [OpenXLab](https://openxlab.org.cn/apps). Try out online demo! [](https://openxlab.org.cn/apps/detail/ShangchenZhou/ProPainter)
|
||||
- **2023.11.09**: Integrated to :hugs: [Hugging Face](https://huggingface.co/spaces). Try out online demo! [](https://huggingface.co/spaces/sczhou/ProPainter)
|
||||
- **2023.09.24**: We remove the watermark removal demos officially to prevent the misuse of our work for unethical purposes.
|
||||
- **2023.09.21**: Add features for memory-efficient inference. Check our [GPU memory](https://github.com/sczhou/ProPainter#-memory-efficient-inference) requirements. 🚀
|
||||
- **2023.09.07**: Our code and model are publicly available. 🐳
|
||||
- **2023.09.01**: This repo is created.
|
||||
|
||||
|
||||
### TODO
|
||||
- [ ] Make a Colab demo.
|
||||
- [x] ~~Make a interactive Gradio demo.~~
|
||||
- [x] ~~Update features for memory-efficient inference.~~
|
||||
|
||||
## Results
|
||||
|
||||
#### 🏂 Object Removal
|
||||
#### 👨🏻🎨 Object Removal
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -56,18 +81,6 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 🌈 Watermark Removal
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="assets/watermark_removal1.gif">
|
||||
</td>
|
||||
<td>
|
||||
<img src="assets/watermark_removal2.gif">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 🎨 Video Completion
|
||||
<table>
|
||||
<tr>
|
||||
@@ -78,16 +91,195 @@
|
||||
<img src="assets/video_completion2.gif">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="assets/video_completion3.gif">
|
||||
</td>
|
||||
<td>
|
||||
<img src="assets/video_completion4.gif">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Overview
|
||||

|
||||
|
||||
|
||||
## Dependencies and Installation
|
||||
|
||||
1. Clone Repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sczhou/ProPainter.git
|
||||
```
|
||||
|
||||
2. Create Conda Environment and Install Dependencies
|
||||
|
||||
```bash
|
||||
# create new anaconda env
|
||||
conda create -n propainter python=3.8 -y
|
||||
conda activate propainter
|
||||
|
||||
# install python dependencies
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
- CUDA >= 9.2
|
||||
- PyTorch >= 1.7.1
|
||||
- Torchvision >= 0.8.2
|
||||
- Other required packages in `requirements.txt`
|
||||
|
||||
## Get Started
|
||||
### Prepare pretrained models
|
||||
Download our pretrained models from [Releases V0.1.0](https://github.com/sczhou/ProPainter/releases/tag/v0.1.0) to the `weights` folder. (All pretrained models can also be automatically downloaded during the first inference.)
|
||||
|
||||
The directory structure will be arranged as:
|
||||
```
|
||||
weights
|
||||
|- ProPainter.pth
|
||||
|- recurrent_flow_completion.pth
|
||||
|- raft-things.pth
|
||||
|- i3d_rgb_imagenet.pt (for evaluating VFID metric)
|
||||
|- README.md
|
||||
```
|
||||
|
||||
### 🏂 Quick test
|
||||
We provide some examples in the [`inputs`](./inputs) folder.
|
||||
Run the following commands to try it out:
|
||||
```shell
|
||||
# The first example (object removal)
|
||||
python inference_propainter.py --video inputs/object_removal/bmx-trees --mask inputs/object_removal/bmx-trees_mask
|
||||
# The second example (video completion)
|
||||
python inference_propainter.py --video inputs/video_completion/running_car.mp4 --mask inputs/video_completion/mask_square.png --height 240 --width 432
|
||||
```
|
||||
|
||||
The results will be saved in the `results` folder.
|
||||
To test your own videos, please prepare the input `mp4 video` (or `split frames`) and `frame-wise mask(s)`.
|
||||
|
||||
If you want to specify the video resolution for processing or avoid running out of memory, you can set the video size of `--width` and `--height`:
|
||||
```shell
|
||||
# process a 576x320 video; set --fp16 to use fp16 (half precision) during inference.
|
||||
python inference_propainter.py --video inputs/video_completion/running_car.mp4 --mask inputs/video_completion/mask_square.png --height 320 --width 576 --fp16
|
||||
```
|
||||
|
||||
#### 💃🏻 Interactive Demo
|
||||
|
||||
We also provide an interactive demo for object removal, allowing users to select any object they wish to remove from a video. You can try the demo on [Hugging Face](https://huggingface.co/spaces/sczhou/ProPainter) or run it [locally](https://github.com/sczhou/ProPainter/tree/main/web-demos/hugging_face).
|
||||
|
||||
<div align="center">
|
||||
<img src="./web-demos/hugging_face/assets/demo.gif" alt="Demo GIF" style="max-width: 512px; height: auto;">
|
||||
</div>
|
||||
|
||||
*Please note that the demo's interface and usage may differ from the GIF animation above. For detailed instructions, refer to the [user guide](https://github.com/sczhou/ProPainter/blob/main/web-demos/hugging_face/README.md).*
|
||||
|
||||
### 🚀 Memory-efficient inference
|
||||
|
||||
Video inpainting typically requires a significant amount of GPU memory. Here, we offer various features that facilitate memory-efficient inference, effectively avoiding the Out-Of-Memory (OOM) error. You can use the following options to reduce memory usage further:
|
||||
|
||||
- Reduce the number of local neighbors through decreasing the `--neighbor_length` (default 10).
|
||||
- Reduce the number of global references by increasing the `--ref_stride` (default 10).
|
||||
- Set the `--resize_ratio` (default 1.0) to resize the processing video.
|
||||
- Set a smaller video size via specifying the `--width` and `--height`.
|
||||
- Set `--fp16` to use fp16 (half precision) during inference.
|
||||
- Reduce the frames of sub-videos `--subvideo_length` (default 80), which effectively decouples GPU memory costs and video length.
|
||||
|
||||
Blow shows the estimated GPU memory requirements for different sub-video lengths with fp32/fp16 precision:
|
||||
|
||||
| Resolution | 50 frames | 80 frames |
|
||||
| :--- | :----: | :----: |
|
||||
| 1280 x 720 | 28G / 19G | OOM / 25G |
|
||||
| 720 x 480 | 11G / 7G | 13G / 8G |
|
||||
| 640 x 480 | 10G / 6G | 12G / 7G |
|
||||
| 320 x 240 | 3G / 2G | 4G / 3G |
|
||||
|
||||
|
||||
## Dataset preparation
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dataset</th>
|
||||
<th>YouTube-VOS</th>
|
||||
<th>DAVIS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>For training (3,471) and evaluation (508)</td>
|
||||
<td>For evaluation (50 in 90)</td>
|
||||
<tr>
|
||||
<td>Images</td>
|
||||
<td> [<a href="https://competitions.codalab.org/competitions/19544#participate-get-data">Official Link</a>] (Download train and test all frames) </td>
|
||||
<td> [<a href="https://data.vision.ee.ethz.ch/csergi/share/davis/DAVIS-2017-trainval-480p.zip">Official Link</a>] (2017, 480p, TrainVal) </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Masks</td>
|
||||
<td colspan="2"> [<a href="https://drive.google.com/file/d/1dFTneS_zaJAHjglxU10gYzr1-xALgHa4/view?usp=sharing">Google Drive</a>] [<a href="https://pan.baidu.com/s/1JC-UKmlQfjhVtD81196cxA?pwd=87e3">Baidu Disk</a>] (For reproducing paper results; provided in <a href="https://arxiv.org/abs/2309.03897">ProPainter</a> paper) </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The training and test split files are provided in `datasets/<dataset_name>`. For each dataset, you should place `JPEGImages` to `datasets/<dataset_name>`. Resize all video frames to size `432x240` for training. Unzip downloaded mask files to `datasets`.
|
||||
|
||||
The `datasets` directory structure will be arranged as: (**Note**: please check it carefully)
|
||||
```
|
||||
datasets
|
||||
|- davis
|
||||
|- JPEGImages_432_240
|
||||
|- <video_name>
|
||||
|- 00000.jpg
|
||||
|- 00001.jpg
|
||||
|- test_masks
|
||||
|- <video_name>
|
||||
|- 00000.png
|
||||
|- 00001.png
|
||||
|- train.json
|
||||
|- test.json
|
||||
|- youtube-vos
|
||||
|- JPEGImages_432_240
|
||||
|- <video_name>
|
||||
|- 00000.jpg
|
||||
|- 00001.jpg
|
||||
|- test_masks
|
||||
|- <video_name>
|
||||
|- 00000.png
|
||||
|- 00001.png
|
||||
|- train.json
|
||||
|- test.json
|
||||
```
|
||||
|
||||
## Training
|
||||
Our training configures are provided in [`train_flowcomp.json`](./configs/train_flowcomp.json) (for Recurrent Flow Completion Network) and [`train_propainter.json`](./configs/train_propainter.json) (for ProPainter).
|
||||
|
||||
Run one of the following commands for training:
|
||||
```shell
|
||||
# For training Recurrent Flow Completion Network
|
||||
python train.py -c configs/train_flowcomp.json
|
||||
# For training ProPainter
|
||||
python train.py -c configs/train_propainter.json
|
||||
```
|
||||
You can run the **same command** to **resume** your training.
|
||||
|
||||
To speed up the training process, you can precompute optical flow for the training dataset using the following command:
|
||||
```shell
|
||||
# Compute optical flow for training dataset
|
||||
python scripts/compute_flow.py --root_path <dataset_root> --save_path <save_flow_root> --height 240 --width 432
|
||||
```
|
||||
|
||||
## Evaluation
|
||||
Run one of the following commands for evaluation:
|
||||
```shell
|
||||
# For evaluating flow completion model
|
||||
python scripts/evaluate_flow_completion.py --dataset <dataset_name> --video_root <video_root> --mask_root <mask_root> --save_results
|
||||
# For evaluating ProPainter model
|
||||
python scripts/evaluate_propainter.py --dataset <dataset_name> --video_root <video_root> --mask_root <mask_root> --save_results
|
||||
```
|
||||
|
||||
The scores and results will also be saved in the `results_eval` folder.
|
||||
Please `--save_results` for further [evaluating temporal warping error](https://github.com/phoenix104104/fast_blind_video_consistency#evaluation).
|
||||
|
||||
|
||||
|
||||
## Citation
|
||||
@@ -106,12 +298,37 @@
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under <a rel="license" href="./LICENSE">NTU S-Lab License 1.0</a>. Redistribution and use should follow this license.
|
||||
#### Non-Commercial Use Only Declaration
|
||||
The ProPainter is made available for use, reproduction, and distribution strictly for non-commercial purposes. The code and models are licensed under <a rel="license" href="./LICENSE">NTU S-Lab License 1.0</a>. Redistribution and use should follow this license.
|
||||
|
||||
For inquiries or to obtain permission for commercial use, please consult Dr. Shangchen Zhou (shangchenzhou@gmail.com).
|
||||
|
||||
|
||||
## Projects that use ProPainter
|
||||
|
||||
If you develop or use ProPainter in your projects, feel free to let me know. Also, please include this [ProPainter](https://github.com/sczhou/ProPainter) repo link, authorship information, and our [S-Lab license](https://github.com/sczhou/ProPainter/blob/main/LICENSE) (with link).
|
||||
|
||||
#### Projects/Applications from the Community
|
||||
|
||||
- Streaming ProPainter: https://github.com/osmr/propainter
|
||||
- Faster ProPainter: https://github.com/halfzm/faster-propainter
|
||||
- ProPainter WebUI: https://github.com/halfzm/ProPainter-Webui
|
||||
- ProPainter ComfyUI: https://github.com/daniabib/ComfyUI_ProPainter_Nodes
|
||||
- Cutie (video segmentation): https://github.com/hkchengrex/Cutie
|
||||
- Cinetransfer (character transfer): https://virtualfilmstudio.github.io/projects/cinetransfer
|
||||
- Motionshop (character transfer): https://aigc3d.github.io/motionshop
|
||||
|
||||
|
||||
|
||||
#### PyPI
|
||||
- propainter: https://pypi.org/project/propainter
|
||||
- pytorchcv: https://pypi.org/project/pytorchcv
|
||||
|
||||
## Contact
|
||||
If you have any questions, please feel free to reach me out at `shangchenzhou@gmail.com`.
|
||||
If you have any questions, please feel free to reach me out at shangchenzhou@gmail.com.
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
This code is based on [E2FGVI](https://github.com/MCG-NKU/E2FGVI) and [STTN](https://github.com/researchmm/STTN). Some code are brought from [BasicVSR++](https://github.com/ckkelvinchan/BasicVSR_PlusPlus). Thanks for their awesome works.
|
||||
This code is based on [E<sup>2</sup>FGVI](https://github.com/MCG-NKU/E2FGVI) and [STTN](https://github.com/researchmm/STTN). Some code are brought from [BasicVSR++](https://github.com/ckkelvinchan/BasicVSR_PlusPlus). Thanks for their awesome works.
|
||||
|
||||
Special thanks to [Yihang Luo](https://github.com/Luo-Yihang) for his valuable contributions to build and maintain the Gradio demos for ProPainter.
|
||||
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 3.8 MiB |
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"seed": 2023,
|
||||
"save_dir": "experiments_model/",
|
||||
"train_data_loader": {
|
||||
"name": "youtube-vos",
|
||||
"video_root": "your_video_root",
|
||||
"flow_root": "your_flow_root",
|
||||
"w": 432,
|
||||
"h": 240,
|
||||
"num_local_frames": 10,
|
||||
"num_ref_frames": 1,
|
||||
"load_flow": 0
|
||||
},
|
||||
"losses": {
|
||||
"flow_weight": 0.25
|
||||
},
|
||||
"model": {
|
||||
"net": "recurrent_flow_completion"
|
||||
},
|
||||
"trainer": {
|
||||
"version": "trainer_flow_w_edge",
|
||||
"type": "Adam",
|
||||
"beta1": 0,
|
||||
"beta2": 0.99,
|
||||
"lr": 5e-5,
|
||||
"batch_size": 8,
|
||||
"num_workers": 4,
|
||||
"num_prefetch_queue": 4,
|
||||
"log_freq": 100,
|
||||
"save_freq": 5e3,
|
||||
"iterations": 700e3,
|
||||
"scheduler": {
|
||||
"type": "MultiStepLR",
|
||||
"milestones": [
|
||||
300e3, 400e3, 500e3, 600e3
|
||||
],
|
||||
"gamma": 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"seed": 2023,
|
||||
"save_dir": "experiments_model/",
|
||||
"train_data_loader": {
|
||||
"name": "youtube-vos",
|
||||
"video_root": "your_video_root",
|
||||
"flow_root": "your_flow_root",
|
||||
"w": 432,
|
||||
"h": 240,
|
||||
"num_local_frames": 10,
|
||||
"num_ref_frames": 6,
|
||||
"load_flow": 0
|
||||
},
|
||||
"losses": {
|
||||
"hole_weight": 1,
|
||||
"valid_weight": 1,
|
||||
"flow_weight": 1,
|
||||
"adversarial_weight": 0.01,
|
||||
"GAN_LOSS": "hinge",
|
||||
"perceptual_weight": 0
|
||||
},
|
||||
"model": {
|
||||
"net": "propainter",
|
||||
"no_dis": 0,
|
||||
"load_d": 1,
|
||||
"interp_mode": "nearest"
|
||||
},
|
||||
"trainer": {
|
||||
"version": "trainer",
|
||||
"type": "Adam",
|
||||
"beta1": 0,
|
||||
"beta2": 0.99,
|
||||
"lr": 1e-4,
|
||||
"batch_size": 8,
|
||||
"num_workers": 8,
|
||||
"num_prefetch_queue": 8,
|
||||
"log_freq": 100,
|
||||
"save_freq": 1e4,
|
||||
"iterations": 700e3,
|
||||
"scheduler": {
|
||||
"type": "MultiStepLR",
|
||||
"milestones": [
|
||||
400e3
|
||||
],
|
||||
"gamma": 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
|
||||
import cv2
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torchvision.transforms as transforms
|
||||
|
||||
from utils.file_client import FileClient
|
||||
from utils.img_util import imfrombytes
|
||||
from utils.flow_util import resize_flow, flowread
|
||||
from core.utils import (create_random_shape_with_random_motion, Stack,
|
||||
ToTorchFormatTensor, GroupRandomHorizontalFlip,GroupRandomHorizontalFlowFlip)
|
||||
|
||||
|
||||
class TrainDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, args: dict):
|
||||
self.args = args
|
||||
self.video_root = args['video_root']
|
||||
self.flow_root = args['flow_root']
|
||||
self.num_local_frames = args['num_local_frames']
|
||||
self.num_ref_frames = args['num_ref_frames']
|
||||
self.size = self.w, self.h = (args['w'], args['h'])
|
||||
|
||||
self.load_flow = args['load_flow']
|
||||
if self.load_flow:
|
||||
assert os.path.exists(self.flow_root)
|
||||
|
||||
json_path = os.path.join('./datasets', args['name'], 'train.json')
|
||||
|
||||
with open(json_path, 'r') as f:
|
||||
self.video_train_dict = json.load(f)
|
||||
self.video_names = sorted(list(self.video_train_dict.keys()))
|
||||
|
||||
# self.video_names = sorted(os.listdir(self.video_root))
|
||||
self.video_dict = {}
|
||||
self.frame_dict = {}
|
||||
|
||||
for v in self.video_names:
|
||||
frame_list = sorted(os.listdir(os.path.join(self.video_root, v)))
|
||||
v_len = len(frame_list)
|
||||
if v_len > self.num_local_frames + self.num_ref_frames:
|
||||
self.video_dict[v] = v_len
|
||||
self.frame_dict[v] = frame_list
|
||||
|
||||
|
||||
self.video_names = list(self.video_dict.keys()) # update names
|
||||
|
||||
self._to_tensors = transforms.Compose([
|
||||
Stack(),
|
||||
ToTorchFormatTensor(),
|
||||
])
|
||||
self.file_client = FileClient('disk')
|
||||
|
||||
def __len__(self):
|
||||
return len(self.video_names)
|
||||
|
||||
def _sample_index(self, length, sample_length, num_ref_frame=3):
|
||||
complete_idx_set = list(range(length))
|
||||
pivot = random.randint(0, length - sample_length)
|
||||
local_idx = complete_idx_set[pivot:pivot + sample_length]
|
||||
remain_idx = list(set(complete_idx_set) - set(local_idx))
|
||||
ref_index = sorted(random.sample(remain_idx, num_ref_frame))
|
||||
|
||||
return local_idx + ref_index
|
||||
|
||||
def __getitem__(self, index):
|
||||
video_name = self.video_names[index]
|
||||
# create masks
|
||||
all_masks = create_random_shape_with_random_motion(
|
||||
self.video_dict[video_name], imageHeight=self.h, imageWidth=self.w)
|
||||
|
||||
# create sample index
|
||||
selected_index = self._sample_index(self.video_dict[video_name],
|
||||
self.num_local_frames,
|
||||
self.num_ref_frames)
|
||||
|
||||
# read video frames
|
||||
frames = []
|
||||
masks = []
|
||||
flows_f, flows_b = [], []
|
||||
for idx in selected_index:
|
||||
frame_list = self.frame_dict[video_name]
|
||||
img_path = os.path.join(self.video_root, video_name, frame_list[idx])
|
||||
img_bytes = self.file_client.get(img_path, 'img')
|
||||
img = imfrombytes(img_bytes, float32=False)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = cv2.resize(img, self.size, interpolation=cv2.INTER_LINEAR)
|
||||
img = Image.fromarray(img)
|
||||
|
||||
frames.append(img)
|
||||
masks.append(all_masks[idx])
|
||||
|
||||
if len(frames) <= self.num_local_frames-1 and self.load_flow:
|
||||
current_n = frame_list[idx][:-4]
|
||||
next_n = frame_list[idx+1][:-4]
|
||||
flow_f_path = os.path.join(self.flow_root, video_name, f'{current_n}_{next_n}_f.flo')
|
||||
flow_b_path = os.path.join(self.flow_root, video_name, f'{next_n}_{current_n}_b.flo')
|
||||
flow_f = flowread(flow_f_path, quantize=False)
|
||||
flow_b = flowread(flow_b_path, quantize=False)
|
||||
flow_f = resize_flow(flow_f, self.h, self.w)
|
||||
flow_b = resize_flow(flow_b, self.h, self.w)
|
||||
flows_f.append(flow_f)
|
||||
flows_b.append(flow_b)
|
||||
|
||||
if len(frames) == self.num_local_frames: # random reverse
|
||||
if random.random() < 0.5:
|
||||
frames.reverse()
|
||||
masks.reverse()
|
||||
if self.load_flow:
|
||||
flows_f.reverse()
|
||||
flows_b.reverse()
|
||||
flows_ = flows_f
|
||||
flows_f = flows_b
|
||||
flows_b = flows_
|
||||
|
||||
if self.load_flow:
|
||||
frames, flows_f, flows_b = GroupRandomHorizontalFlowFlip()(frames, flows_f, flows_b)
|
||||
else:
|
||||
frames = GroupRandomHorizontalFlip()(frames)
|
||||
|
||||
# normalizate, to tensors
|
||||
frame_tensors = self._to_tensors(frames) * 2.0 - 1.0
|
||||
mask_tensors = self._to_tensors(masks)
|
||||
if self.load_flow:
|
||||
flows_f = np.stack(flows_f, axis=-1) # H W 2 T-1
|
||||
flows_b = np.stack(flows_b, axis=-1)
|
||||
flows_f = torch.from_numpy(flows_f).permute(3, 2, 0, 1).contiguous().float()
|
||||
flows_b = torch.from_numpy(flows_b).permute(3, 2, 0, 1).contiguous().float()
|
||||
|
||||
# img [-1,1] mask [0,1]
|
||||
if self.load_flow:
|
||||
return frame_tensors, mask_tensors, flows_f, flows_b, video_name
|
||||
else:
|
||||
return frame_tensors, mask_tensors, 'None', 'None', video_name
|
||||
|
||||
|
||||
class TestDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.size = self.w, self.h = args['size']
|
||||
|
||||
self.video_root = args['video_root']
|
||||
self.mask_root = args['mask_root']
|
||||
self.flow_root = args['flow_root']
|
||||
|
||||
self.load_flow = args['load_flow']
|
||||
if self.load_flow:
|
||||
assert os.path.exists(self.flow_root)
|
||||
self.video_names = sorted(os.listdir(self.mask_root))
|
||||
|
||||
self.video_dict = {}
|
||||
self.frame_dict = {}
|
||||
|
||||
for v in self.video_names:
|
||||
frame_list = sorted(os.listdir(os.path.join(self.video_root, v)))
|
||||
v_len = len(frame_list)
|
||||
self.video_dict[v] = v_len
|
||||
self.frame_dict[v] = frame_list
|
||||
|
||||
self._to_tensors = transforms.Compose([
|
||||
Stack(),
|
||||
ToTorchFormatTensor(),
|
||||
])
|
||||
self.file_client = FileClient('disk')
|
||||
|
||||
def __len__(self):
|
||||
return len(self.video_names)
|
||||
|
||||
def __getitem__(self, index):
|
||||
video_name = self.video_names[index]
|
||||
selected_index = list(range(self.video_dict[video_name]))
|
||||
|
||||
# read video frames
|
||||
frames = []
|
||||
masks = []
|
||||
flows_f, flows_b = [], []
|
||||
for idx in selected_index:
|
||||
frame_list = self.frame_dict[video_name]
|
||||
frame_path = os.path.join(self.video_root, video_name, frame_list[idx])
|
||||
|
||||
img_bytes = self.file_client.get(frame_path, 'input')
|
||||
img = imfrombytes(img_bytes, float32=False)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
img = cv2.resize(img, self.size, interpolation=cv2.INTER_LINEAR)
|
||||
img = Image.fromarray(img)
|
||||
|
||||
frames.append(img)
|
||||
|
||||
mask_path = os.path.join(self.mask_root, video_name, str(idx).zfill(5) + '.png')
|
||||
mask = Image.open(mask_path).resize(self.size, Image.NEAREST).convert('L')
|
||||
|
||||
# origin: 0 indicates missing. now: 1 indicates missing
|
||||
mask = np.asarray(mask)
|
||||
m = np.array(mask > 0).astype(np.uint8)
|
||||
|
||||
m = cv2.dilate(m,
|
||||
cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)),
|
||||
iterations=4)
|
||||
mask = Image.fromarray(m * 255)
|
||||
masks.append(mask)
|
||||
|
||||
if len(frames) <= len(selected_index)-1 and self.load_flow:
|
||||
current_n = frame_list[idx][:-4]
|
||||
next_n = frame_list[idx+1][:-4]
|
||||
flow_f_path = os.path.join(self.flow_root, video_name, f'{current_n}_{next_n}_f.flo')
|
||||
flow_b_path = os.path.join(self.flow_root, video_name, f'{next_n}_{current_n}_b.flo')
|
||||
flow_f = flowread(flow_f_path, quantize=False)
|
||||
flow_b = flowread(flow_b_path, quantize=False)
|
||||
flow_f = resize_flow(flow_f, self.h, self.w)
|
||||
flow_b = resize_flow(flow_b, self.h, self.w)
|
||||
flows_f.append(flow_f)
|
||||
flows_b.append(flow_b)
|
||||
|
||||
# normalizate, to tensors
|
||||
frames_PIL = [np.array(f).astype(np.uint8) for f in frames]
|
||||
frame_tensors = self._to_tensors(frames) * 2.0 - 1.0
|
||||
mask_tensors = self._to_tensors(masks)
|
||||
|
||||
if self.load_flow:
|
||||
flows_f = np.stack(flows_f, axis=-1) # H W 2 T-1
|
||||
flows_b = np.stack(flows_b, axis=-1)
|
||||
flows_f = torch.from_numpy(flows_f).permute(3, 2, 0, 1).contiguous().float()
|
||||
flows_b = torch.from_numpy(flows_b).permute(3, 2, 0, 1).contiguous().float()
|
||||
|
||||
if self.load_flow:
|
||||
return frame_tensors, mask_tensors, flows_f, flows_b, video_name, frames_PIL
|
||||
else:
|
||||
return frame_tensors, mask_tensors, 'None', 'None', video_name
|
||||
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
import torch
|
||||
|
||||
|
||||
def get_world_size():
|
||||
"""Find OMPI world size without calling mpi functions
|
||||
:rtype: int
|
||||
"""
|
||||
if os.environ.get('PMI_SIZE') is not None:
|
||||
return int(os.environ.get('PMI_SIZE') or 1)
|
||||
elif os.environ.get('OMPI_COMM_WORLD_SIZE') is not None:
|
||||
return int(os.environ.get('OMPI_COMM_WORLD_SIZE') or 1)
|
||||
else:
|
||||
return torch.cuda.device_count()
|
||||
|
||||
|
||||
def get_global_rank():
|
||||
"""Find OMPI world rank without calling mpi functions
|
||||
:rtype: int
|
||||
"""
|
||||
if os.environ.get('PMI_RANK') is not None:
|
||||
return int(os.environ.get('PMI_RANK') or 0)
|
||||
elif os.environ.get('OMPI_COMM_WORLD_RANK') is not None:
|
||||
return int(os.environ.get('OMPI_COMM_WORLD_RANK') or 0)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def get_local_rank():
|
||||
"""Find OMPI local rank without calling mpi functions
|
||||
:rtype: int
|
||||
"""
|
||||
if os.environ.get('MPI_LOCALRANKID') is not None:
|
||||
return int(os.environ.get('MPI_LOCALRANKID') or 0)
|
||||
elif os.environ.get('OMPI_COMM_WORLD_LOCAL_RANK') is not None:
|
||||
return int(os.environ.get('OMPI_COMM_WORLD_LOCAL_RANK') or 0)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def get_master_ip():
|
||||
if os.environ.get('AZ_BATCH_MASTER_NODE') is not None:
|
||||
return os.environ.get('AZ_BATCH_MASTER_NODE').split(':')[0]
|
||||
elif os.environ.get('AZ_BATCHAI_MPI_MASTER_NODE') is not None:
|
||||
return os.environ.get('AZ_BATCHAI_MPI_MASTER_NODE')
|
||||
else:
|
||||
return "127.0.0.1"
|
||||
@@ -0,0 +1,180 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import lpips
|
||||
from model.vgg_arch import VGGFeatureExtractor
|
||||
|
||||
class PerceptualLoss(nn.Module):
|
||||
"""Perceptual loss with commonly used style loss.
|
||||
|
||||
Args:
|
||||
layer_weights (dict): The weight for each layer of vgg feature.
|
||||
Here is an example: {'conv5_4': 1.}, which means the conv5_4
|
||||
feature layer (before relu5_4) will be extracted with weight
|
||||
1.0 in calculting losses.
|
||||
vgg_type (str): The type of vgg network used as feature extractor.
|
||||
Default: 'vgg19'.
|
||||
use_input_norm (bool): If True, normalize the input image in vgg.
|
||||
Default: True.
|
||||
range_norm (bool): If True, norm images with range [-1, 1] to [0, 1].
|
||||
Default: False.
|
||||
perceptual_weight (float): If `perceptual_weight > 0`, the perceptual
|
||||
loss will be calculated and the loss will multiplied by the
|
||||
weight. Default: 1.0.
|
||||
style_weight (float): If `style_weight > 0`, the style loss will be
|
||||
calculated and the loss will multiplied by the weight.
|
||||
Default: 0.
|
||||
criterion (str): Criterion used for perceptual loss. Default: 'l1'.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
layer_weights,
|
||||
vgg_type='vgg19',
|
||||
use_input_norm=True,
|
||||
range_norm=False,
|
||||
perceptual_weight=1.0,
|
||||
style_weight=0.,
|
||||
criterion='l1'):
|
||||
super(PerceptualLoss, self).__init__()
|
||||
self.perceptual_weight = perceptual_weight
|
||||
self.style_weight = style_weight
|
||||
self.layer_weights = layer_weights
|
||||
self.vgg = VGGFeatureExtractor(
|
||||
layer_name_list=list(layer_weights.keys()),
|
||||
vgg_type=vgg_type,
|
||||
use_input_norm=use_input_norm,
|
||||
range_norm=range_norm)
|
||||
|
||||
self.criterion_type = criterion
|
||||
if self.criterion_type == 'l1':
|
||||
self.criterion = torch.nn.L1Loss()
|
||||
elif self.criterion_type == 'l2':
|
||||
self.criterion = torch.nn.L2loss()
|
||||
elif self.criterion_type == 'mse':
|
||||
self.criterion = torch.nn.MSELoss(reduction='mean')
|
||||
elif self.criterion_type == 'fro':
|
||||
self.criterion = None
|
||||
else:
|
||||
raise NotImplementedError(f'{criterion} criterion has not been supported.')
|
||||
|
||||
def forward(self, x, gt):
|
||||
"""Forward function.
|
||||
|
||||
Args:
|
||||
x (Tensor): Input tensor with shape (n, c, h, w).
|
||||
gt (Tensor): Ground-truth tensor with shape (n, c, h, w).
|
||||
|
||||
Returns:
|
||||
Tensor: Forward results.
|
||||
"""
|
||||
# extract vgg features
|
||||
x_features = self.vgg(x)
|
||||
gt_features = self.vgg(gt.detach())
|
||||
|
||||
# calculate perceptual loss
|
||||
if self.perceptual_weight > 0:
|
||||
percep_loss = 0
|
||||
for k in x_features.keys():
|
||||
if self.criterion_type == 'fro':
|
||||
percep_loss += torch.norm(x_features[k] - gt_features[k], p='fro') * self.layer_weights[k]
|
||||
else:
|
||||
percep_loss += self.criterion(x_features[k], gt_features[k]) * self.layer_weights[k]
|
||||
percep_loss *= self.perceptual_weight
|
||||
else:
|
||||
percep_loss = None
|
||||
|
||||
# calculate style loss
|
||||
if self.style_weight > 0:
|
||||
style_loss = 0
|
||||
for k in x_features.keys():
|
||||
if self.criterion_type == 'fro':
|
||||
style_loss += torch.norm(
|
||||
self._gram_mat(x_features[k]) - self._gram_mat(gt_features[k]), p='fro') * self.layer_weights[k]
|
||||
else:
|
||||
style_loss += self.criterion(self._gram_mat(x_features[k]), self._gram_mat(
|
||||
gt_features[k])) * self.layer_weights[k]
|
||||
style_loss *= self.style_weight
|
||||
else:
|
||||
style_loss = None
|
||||
|
||||
return percep_loss, style_loss
|
||||
|
||||
def _gram_mat(self, x):
|
||||
"""Calculate Gram matrix.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Tensor with shape of (n, c, h, w).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Gram matrix.
|
||||
"""
|
||||
n, c, h, w = x.size()
|
||||
features = x.view(n, c, w * h)
|
||||
features_t = features.transpose(1, 2)
|
||||
gram = features.bmm(features_t) / (c * h * w)
|
||||
return gram
|
||||
|
||||
class LPIPSLoss(nn.Module):
|
||||
def __init__(self,
|
||||
loss_weight=1.0,
|
||||
use_input_norm=True,
|
||||
range_norm=False,):
|
||||
super(LPIPSLoss, self).__init__()
|
||||
self.perceptual = lpips.LPIPS(net="vgg", spatial=False).eval()
|
||||
self.loss_weight = loss_weight
|
||||
self.use_input_norm = use_input_norm
|
||||
self.range_norm = range_norm
|
||||
|
||||
if self.use_input_norm:
|
||||
# the mean is for image with range [0, 1]
|
||||
self.register_buffer('mean', torch.Tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
|
||||
# the std is for image with range [0, 1]
|
||||
self.register_buffer('std', torch.Tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
|
||||
|
||||
def forward(self, pred, target):
|
||||
if self.range_norm:
|
||||
pred = (pred + 1) / 2
|
||||
target = (target + 1) / 2
|
||||
if self.use_input_norm:
|
||||
pred = (pred - self.mean) / self.std
|
||||
target = (target - self.mean) / self.std
|
||||
lpips_loss = self.perceptual(target.contiguous(), pred.contiguous())
|
||||
return self.loss_weight * lpips_loss.mean(), None
|
||||
|
||||
|
||||
class AdversarialLoss(nn.Module):
|
||||
r"""
|
||||
Adversarial loss
|
||||
https://arxiv.org/abs/1711.10337
|
||||
"""
|
||||
def __init__(self,
|
||||
type='nsgan',
|
||||
target_real_label=1.0,
|
||||
target_fake_label=0.0):
|
||||
r"""
|
||||
type = nsgan | lsgan | hinge
|
||||
"""
|
||||
super(AdversarialLoss, self).__init__()
|
||||
self.type = type
|
||||
self.register_buffer('real_label', torch.tensor(target_real_label))
|
||||
self.register_buffer('fake_label', torch.tensor(target_fake_label))
|
||||
|
||||
if type == 'nsgan':
|
||||
self.criterion = nn.BCELoss()
|
||||
elif type == 'lsgan':
|
||||
self.criterion = nn.MSELoss()
|
||||
elif type == 'hinge':
|
||||
self.criterion = nn.ReLU()
|
||||
|
||||
def __call__(self, outputs, is_real, is_disc=None):
|
||||
if self.type == 'hinge':
|
||||
if is_disc:
|
||||
if is_real:
|
||||
outputs = -outputs
|
||||
return self.criterion(1 + outputs).mean()
|
||||
else:
|
||||
return (-outputs).mean()
|
||||
else:
|
||||
labels = (self.real_label
|
||||
if is_real else self.fake_label).expand_as(outputs)
|
||||
loss = self.criterion(outputs, labels)
|
||||
return loss
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
LR scheduler from BasicSR https://github.com/xinntao/BasicSR
|
||||
"""
|
||||
import math
|
||||
from collections import Counter
|
||||
from torch.optim.lr_scheduler import _LRScheduler
|
||||
|
||||
|
||||
class MultiStepRestartLR(_LRScheduler):
|
||||
""" MultiStep with restarts learning rate scheme.
|
||||
Args:
|
||||
optimizer (torch.nn.optimizer): Torch optimizer.
|
||||
milestones (list): Iterations that will decrease learning rate.
|
||||
gamma (float): Decrease ratio. Default: 0.1.
|
||||
restarts (list): Restart iterations. Default: [0].
|
||||
restart_weights (list): Restart weights at each restart iteration.
|
||||
Default: [1].
|
||||
last_epoch (int): Used in _LRScheduler. Default: -1.
|
||||
"""
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
milestones,
|
||||
gamma=0.1,
|
||||
restarts=(0, ),
|
||||
restart_weights=(1, ),
|
||||
last_epoch=-1):
|
||||
self.milestones = Counter(milestones)
|
||||
self.gamma = gamma
|
||||
self.restarts = restarts
|
||||
self.restart_weights = restart_weights
|
||||
assert len(self.restarts) == len(
|
||||
self.restart_weights), 'restarts and their weights do not match.'
|
||||
super(MultiStepRestartLR, self).__init__(optimizer, last_epoch)
|
||||
|
||||
def get_lr(self):
|
||||
if self.last_epoch in self.restarts:
|
||||
weight = self.restart_weights[self.restarts.index(self.last_epoch)]
|
||||
return [
|
||||
group['initial_lr'] * weight
|
||||
for group in self.optimizer.param_groups
|
||||
]
|
||||
if self.last_epoch not in self.milestones:
|
||||
return [group['lr'] for group in self.optimizer.param_groups]
|
||||
return [
|
||||
group['lr'] * self.gamma**self.milestones[self.last_epoch]
|
||||
for group in self.optimizer.param_groups
|
||||
]
|
||||
|
||||
|
||||
def get_position_from_periods(iteration, cumulative_period):
|
||||
"""Get the position from a period list.
|
||||
It will return the index of the right-closest number in the period list.
|
||||
For example, the cumulative_period = [100, 200, 300, 400],
|
||||
if iteration == 50, return 0;
|
||||
if iteration == 210, return 2;
|
||||
if iteration == 300, return 2.
|
||||
Args:
|
||||
iteration (int): Current iteration.
|
||||
cumulative_period (list[int]): Cumulative period list.
|
||||
Returns:
|
||||
int: The position of the right-closest number in the period list.
|
||||
"""
|
||||
for i, period in enumerate(cumulative_period):
|
||||
if iteration <= period:
|
||||
return i
|
||||
|
||||
|
||||
class CosineAnnealingRestartLR(_LRScheduler):
|
||||
""" Cosine annealing with restarts learning rate scheme.
|
||||
An example of config:
|
||||
periods = [10, 10, 10, 10]
|
||||
restart_weights = [1, 0.5, 0.5, 0.5]
|
||||
eta_min=1e-7
|
||||
It has four cycles, each has 10 iterations. At 10th, 20th, 30th, the
|
||||
scheduler will restart with the weights in restart_weights.
|
||||
Args:
|
||||
optimizer (torch.nn.optimizer): Torch optimizer.
|
||||
periods (list): Period for each cosine anneling cycle.
|
||||
restart_weights (list): Restart weights at each restart iteration.
|
||||
Default: [1].
|
||||
eta_min (float): The mimimum lr. Default: 0.
|
||||
last_epoch (int): Used in _LRScheduler. Default: -1.
|
||||
"""
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
periods,
|
||||
restart_weights=(1, ),
|
||||
eta_min=1e-7,
|
||||
last_epoch=-1):
|
||||
self.periods = periods
|
||||
self.restart_weights = restart_weights
|
||||
self.eta_min = eta_min
|
||||
assert (len(self.periods) == len(self.restart_weights)
|
||||
), 'periods and restart_weights should have the same length.'
|
||||
self.cumulative_period = [
|
||||
sum(self.periods[0:i + 1]) for i in range(0, len(self.periods))
|
||||
]
|
||||
super(CosineAnnealingRestartLR, self).__init__(optimizer, last_epoch)
|
||||
|
||||
def get_lr(self):
|
||||
idx = get_position_from_periods(self.last_epoch,
|
||||
self.cumulative_period)
|
||||
current_weight = self.restart_weights[idx]
|
||||
nearest_restart = 0 if idx == 0 else self.cumulative_period[idx - 1]
|
||||
current_period = self.periods[idx]
|
||||
|
||||
return [
|
||||
self.eta_min + current_weight * 0.5 * (base_lr - self.eta_min) *
|
||||
(1 + math.cos(math.pi * (
|
||||
(self.last_epoch - nearest_restart) / current_period)))
|
||||
for base_lr in self.base_lrs
|
||||
]
|
||||
@@ -0,0 +1,569 @@
|
||||
import numpy as np
|
||||
from skimage import measure
|
||||
from scipy import linalg
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from core.utils import to_tensors
|
||||
|
||||
|
||||
def calculate_epe(flow1, flow2):
|
||||
"""Calculate End point errors."""
|
||||
|
||||
epe = torch.sum((flow1 - flow2)**2, dim=1).sqrt()
|
||||
epe = epe.view(-1)
|
||||
return epe.mean().item()
|
||||
|
||||
|
||||
def calculate_psnr(img1, img2):
|
||||
"""Calculate PSNR (Peak Signal-to-Noise Ratio).
|
||||
Ref: https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
|
||||
Args:
|
||||
img1 (ndarray): Images with range [0, 255].
|
||||
img2 (ndarray): Images with range [0, 255].
|
||||
Returns:
|
||||
float: psnr result.
|
||||
"""
|
||||
|
||||
assert img1.shape == img2.shape, \
|
||||
(f'Image shapes are differnet: {img1.shape}, {img2.shape}.')
|
||||
|
||||
mse = np.mean((img1 - img2)**2)
|
||||
if mse == 0:
|
||||
return float('inf')
|
||||
return 20. * np.log10(255. / np.sqrt(mse))
|
||||
|
||||
|
||||
def calc_psnr_and_ssim(img1, img2):
|
||||
"""Calculate PSNR and SSIM for images.
|
||||
img1: ndarray, range [0, 255]
|
||||
img2: ndarray, range [0, 255]
|
||||
"""
|
||||
img1 = img1.astype(np.float64)
|
||||
img2 = img2.astype(np.float64)
|
||||
|
||||
psnr = calculate_psnr(img1, img2)
|
||||
ssim = measure.compare_ssim(img1,
|
||||
img2,
|
||||
data_range=255,
|
||||
multichannel=True,
|
||||
win_size=65)
|
||||
|
||||
return psnr, ssim
|
||||
|
||||
|
||||
###########################
|
||||
# I3D models
|
||||
###########################
|
||||
|
||||
|
||||
def init_i3d_model(i3d_model_path):
|
||||
print(f"[Loading I3D model from {i3d_model_path} for FID score ..]")
|
||||
i3d_model = InceptionI3d(400, in_channels=3, final_endpoint='Logits')
|
||||
i3d_model.load_state_dict(torch.load(i3d_model_path))
|
||||
i3d_model.to(torch.device('cuda:0'))
|
||||
return i3d_model
|
||||
|
||||
|
||||
def calculate_i3d_activations(video1, video2, i3d_model, device):
|
||||
"""Calculate VFID metric.
|
||||
video1: list[PIL.Image]
|
||||
video2: list[PIL.Image]
|
||||
"""
|
||||
video1 = to_tensors()(video1).unsqueeze(0).to(device)
|
||||
video2 = to_tensors()(video2).unsqueeze(0).to(device)
|
||||
video1_activations = get_i3d_activations(
|
||||
video1, i3d_model).cpu().numpy().flatten()
|
||||
video2_activations = get_i3d_activations(
|
||||
video2, i3d_model).cpu().numpy().flatten()
|
||||
|
||||
return video1_activations, video2_activations
|
||||
|
||||
|
||||
def calculate_vfid(real_activations, fake_activations):
|
||||
"""
|
||||
Given two distribution of features, compute the FID score between them
|
||||
Params:
|
||||
real_activations: list[ndarray]
|
||||
fake_activations: list[ndarray]
|
||||
"""
|
||||
m1 = np.mean(real_activations, axis=0)
|
||||
m2 = np.mean(fake_activations, axis=0)
|
||||
s1 = np.cov(real_activations, rowvar=False)
|
||||
s2 = np.cov(fake_activations, rowvar=False)
|
||||
return calculate_frechet_distance(m1, s1, m2, s2)
|
||||
|
||||
|
||||
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
|
||||
representive data set.
|
||||
-- sigma1: The covariance matrix over activations for generated samples.
|
||||
-- sigma2: The covariance matrix over activations, precalculated on an
|
||||
representive 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):
|
||||
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) + # NOQA
|
||||
np.trace(sigma2) - 2 * tr_covmean)
|
||||
|
||||
|
||||
def get_i3d_activations(batched_video,
|
||||
i3d_model,
|
||||
target_endpoint='Logits',
|
||||
flatten=True,
|
||||
grad_enabled=False):
|
||||
"""
|
||||
Get features from i3d model and flatten them to 1d feature,
|
||||
valid target endpoints are defined in InceptionI3d.VALID_ENDPOINTS
|
||||
VALID_ENDPOINTS = (
|
||||
'Conv3d_1a_7x7',
|
||||
'MaxPool3d_2a_3x3',
|
||||
'Conv3d_2b_1x1',
|
||||
'Conv3d_2c_3x3',
|
||||
'MaxPool3d_3a_3x3',
|
||||
'Mixed_3b',
|
||||
'Mixed_3c',
|
||||
'MaxPool3d_4a_3x3',
|
||||
'Mixed_4b',
|
||||
'Mixed_4c',
|
||||
'Mixed_4d',
|
||||
'Mixed_4e',
|
||||
'Mixed_4f',
|
||||
'MaxPool3d_5a_2x2',
|
||||
'Mixed_5b',
|
||||
'Mixed_5c',
|
||||
'Logits',
|
||||
'Predictions',
|
||||
)
|
||||
"""
|
||||
with torch.set_grad_enabled(grad_enabled):
|
||||
feat = i3d_model.extract_features(batched_video.transpose(1, 2),
|
||||
target_endpoint)
|
||||
if flatten:
|
||||
feat = feat.view(feat.size(0), -1)
|
||||
|
||||
return feat
|
||||
|
||||
|
||||
# This code is from https://github.com/piergiaj/pytorch-i3d/blob/master/pytorch_i3d.py
|
||||
# I only fix flake8 errors and do some cleaning here
|
||||
|
||||
|
||||
class MaxPool3dSamePadding(nn.MaxPool3d):
|
||||
def compute_pad(self, dim, s):
|
||||
if s % self.stride[dim] == 0:
|
||||
return max(self.kernel_size[dim] - self.stride[dim], 0)
|
||||
else:
|
||||
return max(self.kernel_size[dim] - (s % self.stride[dim]), 0)
|
||||
|
||||
def forward(self, x):
|
||||
# compute 'same' padding
|
||||
(batch, channel, t, h, w) = x.size()
|
||||
pad_t = self.compute_pad(0, t)
|
||||
pad_h = self.compute_pad(1, h)
|
||||
pad_w = self.compute_pad(2, w)
|
||||
|
||||
pad_t_f = pad_t // 2
|
||||
pad_t_b = pad_t - pad_t_f
|
||||
pad_h_f = pad_h // 2
|
||||
pad_h_b = pad_h - pad_h_f
|
||||
pad_w_f = pad_w // 2
|
||||
pad_w_b = pad_w - pad_w_f
|
||||
|
||||
pad = (pad_w_f, pad_w_b, pad_h_f, pad_h_b, pad_t_f, pad_t_b)
|
||||
x = F.pad(x, pad)
|
||||
return super(MaxPool3dSamePadding, self).forward(x)
|
||||
|
||||
|
||||
class Unit3D(nn.Module):
|
||||
def __init__(self,
|
||||
in_channels,
|
||||
output_channels,
|
||||
kernel_shape=(1, 1, 1),
|
||||
stride=(1, 1, 1),
|
||||
padding=0,
|
||||
activation_fn=F.relu,
|
||||
use_batch_norm=True,
|
||||
use_bias=False,
|
||||
name='unit_3d'):
|
||||
"""Initializes Unit3D module."""
|
||||
super(Unit3D, self).__init__()
|
||||
|
||||
self._output_channels = output_channels
|
||||
self._kernel_shape = kernel_shape
|
||||
self._stride = stride
|
||||
self._use_batch_norm = use_batch_norm
|
||||
self._activation_fn = activation_fn
|
||||
self._use_bias = use_bias
|
||||
self.name = name
|
||||
self.padding = padding
|
||||
|
||||
self.conv3d = nn.Conv3d(
|
||||
in_channels=in_channels,
|
||||
out_channels=self._output_channels,
|
||||
kernel_size=self._kernel_shape,
|
||||
stride=self._stride,
|
||||
padding=0, # we always want padding to be 0 here. We will
|
||||
# dynamically pad based on input size in forward function
|
||||
bias=self._use_bias)
|
||||
|
||||
if self._use_batch_norm:
|
||||
self.bn = nn.BatchNorm3d(self._output_channels,
|
||||
eps=0.001,
|
||||
momentum=0.01)
|
||||
|
||||
def compute_pad(self, dim, s):
|
||||
if s % self._stride[dim] == 0:
|
||||
return max(self._kernel_shape[dim] - self._stride[dim], 0)
|
||||
else:
|
||||
return max(self._kernel_shape[dim] - (s % self._stride[dim]), 0)
|
||||
|
||||
def forward(self, x):
|
||||
# compute 'same' padding
|
||||
(batch, channel, t, h, w) = x.size()
|
||||
pad_t = self.compute_pad(0, t)
|
||||
pad_h = self.compute_pad(1, h)
|
||||
pad_w = self.compute_pad(2, w)
|
||||
|
||||
pad_t_f = pad_t // 2
|
||||
pad_t_b = pad_t - pad_t_f
|
||||
pad_h_f = pad_h // 2
|
||||
pad_h_b = pad_h - pad_h_f
|
||||
pad_w_f = pad_w // 2
|
||||
pad_w_b = pad_w - pad_w_f
|
||||
|
||||
pad = (pad_w_f, pad_w_b, pad_h_f, pad_h_b, pad_t_f, pad_t_b)
|
||||
x = F.pad(x, pad)
|
||||
|
||||
x = self.conv3d(x)
|
||||
if self._use_batch_norm:
|
||||
x = self.bn(x)
|
||||
if self._activation_fn is not None:
|
||||
x = self._activation_fn(x)
|
||||
return x
|
||||
|
||||
|
||||
class InceptionModule(nn.Module):
|
||||
def __init__(self, in_channels, out_channels, name):
|
||||
super(InceptionModule, self).__init__()
|
||||
|
||||
self.b0 = Unit3D(in_channels=in_channels,
|
||||
output_channels=out_channels[0],
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
name=name + '/Branch_0/Conv3d_0a_1x1')
|
||||
self.b1a = Unit3D(in_channels=in_channels,
|
||||
output_channels=out_channels[1],
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
name=name + '/Branch_1/Conv3d_0a_1x1')
|
||||
self.b1b = Unit3D(in_channels=out_channels[1],
|
||||
output_channels=out_channels[2],
|
||||
kernel_shape=[3, 3, 3],
|
||||
name=name + '/Branch_1/Conv3d_0b_3x3')
|
||||
self.b2a = Unit3D(in_channels=in_channels,
|
||||
output_channels=out_channels[3],
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
name=name + '/Branch_2/Conv3d_0a_1x1')
|
||||
self.b2b = Unit3D(in_channels=out_channels[3],
|
||||
output_channels=out_channels[4],
|
||||
kernel_shape=[3, 3, 3],
|
||||
name=name + '/Branch_2/Conv3d_0b_3x3')
|
||||
self.b3a = MaxPool3dSamePadding(kernel_size=[3, 3, 3],
|
||||
stride=(1, 1, 1),
|
||||
padding=0)
|
||||
self.b3b = Unit3D(in_channels=in_channels,
|
||||
output_channels=out_channels[5],
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
name=name + '/Branch_3/Conv3d_0b_1x1')
|
||||
self.name = name
|
||||
|
||||
def forward(self, x):
|
||||
b0 = self.b0(x)
|
||||
b1 = self.b1b(self.b1a(x))
|
||||
b2 = self.b2b(self.b2a(x))
|
||||
b3 = self.b3b(self.b3a(x))
|
||||
return torch.cat([b0, b1, b2, b3], dim=1)
|
||||
|
||||
|
||||
class InceptionI3d(nn.Module):
|
||||
"""Inception-v1 I3D architecture.
|
||||
The model is introduced in:
|
||||
Quo Vadis, Action Recognition? A New Model and the Kinetics Dataset
|
||||
Joao Carreira, Andrew Zisserman
|
||||
https://arxiv.org/pdf/1705.07750v1.pdf.
|
||||
See also the Inception architecture, introduced in:
|
||||
Going deeper with convolutions
|
||||
Christian Szegedy, Wei Liu, Yangqing Jia, Pierre Sermanet, Scott Reed,
|
||||
Dragomir Anguelov, Dumitru Erhan, Vincent Vanhoucke, Andrew Rabinovich.
|
||||
http://arxiv.org/pdf/1409.4842v1.pdf.
|
||||
"""
|
||||
|
||||
# Endpoints of the model in order. During construction, all the endpoints up
|
||||
# to a designated `final_endpoint` are returned in a dictionary as the
|
||||
# second return value.
|
||||
VALID_ENDPOINTS = (
|
||||
'Conv3d_1a_7x7',
|
||||
'MaxPool3d_2a_3x3',
|
||||
'Conv3d_2b_1x1',
|
||||
'Conv3d_2c_3x3',
|
||||
'MaxPool3d_3a_3x3',
|
||||
'Mixed_3b',
|
||||
'Mixed_3c',
|
||||
'MaxPool3d_4a_3x3',
|
||||
'Mixed_4b',
|
||||
'Mixed_4c',
|
||||
'Mixed_4d',
|
||||
'Mixed_4e',
|
||||
'Mixed_4f',
|
||||
'MaxPool3d_5a_2x2',
|
||||
'Mixed_5b',
|
||||
'Mixed_5c',
|
||||
'Logits',
|
||||
'Predictions',
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
num_classes=400,
|
||||
spatial_squeeze=True,
|
||||
final_endpoint='Logits',
|
||||
name='inception_i3d',
|
||||
in_channels=3,
|
||||
dropout_keep_prob=0.5):
|
||||
"""Initializes I3D model instance.
|
||||
Args:
|
||||
num_classes: The number of outputs in the logit layer (default 400, which
|
||||
matches the Kinetics dataset).
|
||||
spatial_squeeze: Whether to squeeze the spatial dimensions for the logits
|
||||
before returning (default True).
|
||||
final_endpoint: The model contains many possible endpoints.
|
||||
`final_endpoint` specifies the last endpoint for the model to be built
|
||||
up to. In addition to the output at `final_endpoint`, all the outputs
|
||||
at endpoints up to `final_endpoint` will also be returned, in a
|
||||
dictionary. `final_endpoint` must be one of
|
||||
InceptionI3d.VALID_ENDPOINTS (default 'Logits').
|
||||
name: A string (optional). The name of this module.
|
||||
Raises:
|
||||
ValueError: if `final_endpoint` is not recognized.
|
||||
"""
|
||||
|
||||
if final_endpoint not in self.VALID_ENDPOINTS:
|
||||
raise ValueError('Unknown final endpoint %s' % final_endpoint)
|
||||
|
||||
super(InceptionI3d, self).__init__()
|
||||
self._num_classes = num_classes
|
||||
self._spatial_squeeze = spatial_squeeze
|
||||
self._final_endpoint = final_endpoint
|
||||
self.logits = None
|
||||
|
||||
if self._final_endpoint not in self.VALID_ENDPOINTS:
|
||||
raise ValueError('Unknown final endpoint %s' %
|
||||
self._final_endpoint)
|
||||
|
||||
self.end_points = {}
|
||||
end_point = 'Conv3d_1a_7x7'
|
||||
self.end_points[end_point] = Unit3D(in_channels=in_channels,
|
||||
output_channels=64,
|
||||
kernel_shape=[7, 7, 7],
|
||||
stride=(2, 2, 2),
|
||||
padding=(3, 3, 3),
|
||||
name=name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'MaxPool3d_2a_3x3'
|
||||
self.end_points[end_point] = MaxPool3dSamePadding(
|
||||
kernel_size=[1, 3, 3], stride=(1, 2, 2), padding=0)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Conv3d_2b_1x1'
|
||||
self.end_points[end_point] = Unit3D(in_channels=64,
|
||||
output_channels=64,
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
name=name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Conv3d_2c_3x3'
|
||||
self.end_points[end_point] = Unit3D(in_channels=64,
|
||||
output_channels=192,
|
||||
kernel_shape=[3, 3, 3],
|
||||
padding=1,
|
||||
name=name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'MaxPool3d_3a_3x3'
|
||||
self.end_points[end_point] = MaxPool3dSamePadding(
|
||||
kernel_size=[1, 3, 3], stride=(1, 2, 2), padding=0)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_3b'
|
||||
self.end_points[end_point] = InceptionModule(192,
|
||||
[64, 96, 128, 16, 32, 32],
|
||||
name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_3c'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
256, [128, 128, 192, 32, 96, 64], name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'MaxPool3d_4a_3x3'
|
||||
self.end_points[end_point] = MaxPool3dSamePadding(
|
||||
kernel_size=[3, 3, 3], stride=(2, 2, 2), padding=0)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_4b'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
128 + 192 + 96 + 64, [192, 96, 208, 16, 48, 64], name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_4c'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
192 + 208 + 48 + 64, [160, 112, 224, 24, 64, 64], name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_4d'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
160 + 224 + 64 + 64, [128, 128, 256, 24, 64, 64], name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_4e'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
128 + 256 + 64 + 64, [112, 144, 288, 32, 64, 64], name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_4f'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
112 + 288 + 64 + 64, [256, 160, 320, 32, 128, 128],
|
||||
name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'MaxPool3d_5a_2x2'
|
||||
self.end_points[end_point] = MaxPool3dSamePadding(
|
||||
kernel_size=[2, 2, 2], stride=(2, 2, 2), padding=0)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_5b'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
256 + 320 + 128 + 128, [256, 160, 320, 32, 128, 128],
|
||||
name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Mixed_5c'
|
||||
self.end_points[end_point] = InceptionModule(
|
||||
256 + 320 + 128 + 128, [384, 192, 384, 48, 128, 128],
|
||||
name + end_point)
|
||||
if self._final_endpoint == end_point:
|
||||
return
|
||||
|
||||
end_point = 'Logits'
|
||||
self.avg_pool = nn.AvgPool3d(kernel_size=[2, 7, 7], stride=(1, 1, 1))
|
||||
self.dropout = nn.Dropout(dropout_keep_prob)
|
||||
self.logits = Unit3D(in_channels=384 + 384 + 128 + 128,
|
||||
output_channels=self._num_classes,
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
activation_fn=None,
|
||||
use_batch_norm=False,
|
||||
use_bias=True,
|
||||
name='logits')
|
||||
|
||||
self.build()
|
||||
|
||||
def replace_logits(self, num_classes):
|
||||
self._num_classes = num_classes
|
||||
self.logits = Unit3D(in_channels=384 + 384 + 128 + 128,
|
||||
output_channels=self._num_classes,
|
||||
kernel_shape=[1, 1, 1],
|
||||
padding=0,
|
||||
activation_fn=None,
|
||||
use_batch_norm=False,
|
||||
use_bias=True,
|
||||
name='logits')
|
||||
|
||||
def build(self):
|
||||
for k in self.end_points.keys():
|
||||
self.add_module(k, self.end_points[k])
|
||||
|
||||
def forward(self, x):
|
||||
for end_point in self.VALID_ENDPOINTS:
|
||||
if end_point in self.end_points:
|
||||
x = self._modules[end_point](
|
||||
x) # use _modules to work with dataparallel
|
||||
|
||||
x = self.logits(self.dropout(self.avg_pool(x)))
|
||||
if self._spatial_squeeze:
|
||||
logits = x.squeeze(3).squeeze(3)
|
||||
# logits is batch X time X classes, which is what we want to work with
|
||||
return logits
|
||||
|
||||
def extract_features(self, x, target_endpoint='Logits'):
|
||||
for end_point in self.VALID_ENDPOINTS:
|
||||
if end_point in self.end_points:
|
||||
x = self._modules[end_point](x)
|
||||
if end_point == target_endpoint:
|
||||
break
|
||||
if target_endpoint == 'Logits':
|
||||
return x.mean(4).mean(3).mean(2)
|
||||
else:
|
||||
return x
|
||||
@@ -0,0 +1,125 @@
|
||||
import queue as Queue
|
||||
import threading
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
|
||||
class PrefetchGenerator(threading.Thread):
|
||||
"""A general prefetch generator.
|
||||
|
||||
Ref:
|
||||
https://stackoverflow.com/questions/7323664/python-generator-pre-fetch
|
||||
|
||||
Args:
|
||||
generator: Python generator.
|
||||
num_prefetch_queue (int): Number of prefetch queue.
|
||||
"""
|
||||
|
||||
def __init__(self, generator, num_prefetch_queue):
|
||||
threading.Thread.__init__(self)
|
||||
self.queue = Queue.Queue(num_prefetch_queue)
|
||||
self.generator = generator
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
for item in self.generator:
|
||||
self.queue.put(item)
|
||||
self.queue.put(None)
|
||||
|
||||
def __next__(self):
|
||||
next_item = self.queue.get()
|
||||
if next_item is None:
|
||||
raise StopIteration
|
||||
return next_item
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
class PrefetchDataLoader(DataLoader):
|
||||
"""Prefetch version of dataloader.
|
||||
|
||||
Ref:
|
||||
https://github.com/IgorSusmelj/pytorch-styleguide/issues/5#
|
||||
|
||||
TODO:
|
||||
Need to test on single gpu and ddp (multi-gpu). There is a known issue in
|
||||
ddp.
|
||||
|
||||
Args:
|
||||
num_prefetch_queue (int): Number of prefetch queue.
|
||||
kwargs (dict): Other arguments for dataloader.
|
||||
"""
|
||||
|
||||
def __init__(self, num_prefetch_queue, **kwargs):
|
||||
self.num_prefetch_queue = num_prefetch_queue
|
||||
super(PrefetchDataLoader, self).__init__(**kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
return PrefetchGenerator(super().__iter__(), self.num_prefetch_queue)
|
||||
|
||||
|
||||
class CPUPrefetcher():
|
||||
"""CPU prefetcher.
|
||||
|
||||
Args:
|
||||
loader: Dataloader.
|
||||
"""
|
||||
|
||||
def __init__(self, loader):
|
||||
self.ori_loader = loader
|
||||
self.loader = iter(loader)
|
||||
|
||||
def next(self):
|
||||
try:
|
||||
return next(self.loader)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
def reset(self):
|
||||
self.loader = iter(self.ori_loader)
|
||||
|
||||
|
||||
class CUDAPrefetcher():
|
||||
"""CUDA prefetcher.
|
||||
|
||||
Ref:
|
||||
https://github.com/NVIDIA/apex/issues/304#
|
||||
|
||||
It may consums more GPU memory.
|
||||
|
||||
Args:
|
||||
loader: Dataloader.
|
||||
opt (dict): Options.
|
||||
"""
|
||||
|
||||
def __init__(self, loader, opt):
|
||||
self.ori_loader = loader
|
||||
self.loader = iter(loader)
|
||||
self.opt = opt
|
||||
self.stream = torch.cuda.Stream()
|
||||
self.device = torch.device('cuda' if opt['num_gpu'] != 0 else 'cpu')
|
||||
self.preload()
|
||||
|
||||
def preload(self):
|
||||
try:
|
||||
self.batch = next(self.loader) # self.batch is a dict
|
||||
except StopIteration:
|
||||
self.batch = None
|
||||
return None
|
||||
# put tensors to gpu
|
||||
with torch.cuda.stream(self.stream):
|
||||
for k, v in self.batch.items():
|
||||
if torch.is_tensor(v):
|
||||
self.batch[k] = self.batch[k].to(device=self.device, non_blocking=True)
|
||||
|
||||
def next(self):
|
||||
torch.cuda.current_stream().wait_stream(self.stream)
|
||||
batch = self.batch
|
||||
self.preload()
|
||||
return batch
|
||||
|
||||
def reset(self):
|
||||
self.loader = iter(self.ori_loader)
|
||||
self.preload()
|
||||
@@ -0,0 +1,509 @@
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
import importlib
|
||||
from tqdm import tqdm
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from core.prefetch_dataloader import PrefetchDataLoader, CPUPrefetcher
|
||||
from torch.utils.data.distributed import DistributedSampler
|
||||
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||
import torchvision
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
|
||||
from core.lr_scheduler import MultiStepRestartLR, CosineAnnealingRestartLR
|
||||
from core.loss import AdversarialLoss, PerceptualLoss, LPIPSLoss
|
||||
from core.dataset import TrainDataset
|
||||
|
||||
from model.modules.flow_comp_raft import RAFT_bi, FlowLoss, EdgeLoss
|
||||
from model.recurrent_flow_completion import RecurrentFlowCompleteNet
|
||||
|
||||
from RAFT.utils.flow_viz_pt import flow_to_image
|
||||
|
||||
|
||||
class Trainer:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.epoch = 0
|
||||
self.iteration = 0
|
||||
self.num_local_frames = config['train_data_loader']['num_local_frames']
|
||||
self.num_ref_frames = config['train_data_loader']['num_ref_frames']
|
||||
|
||||
# setup data set and data loader
|
||||
self.train_dataset = TrainDataset(config['train_data_loader'])
|
||||
|
||||
self.train_sampler = None
|
||||
self.train_args = config['trainer']
|
||||
if config['distributed']:
|
||||
self.train_sampler = DistributedSampler(
|
||||
self.train_dataset,
|
||||
num_replicas=config['world_size'],
|
||||
rank=config['global_rank'])
|
||||
|
||||
dataloader_args = dict(
|
||||
dataset=self.train_dataset,
|
||||
batch_size=self.train_args['batch_size'] // config['world_size'],
|
||||
shuffle=(self.train_sampler is None),
|
||||
num_workers=self.train_args['num_workers'],
|
||||
sampler=self.train_sampler,
|
||||
drop_last=True)
|
||||
|
||||
self.train_loader = PrefetchDataLoader(self.train_args['num_prefetch_queue'], **dataloader_args)
|
||||
self.prefetcher = CPUPrefetcher(self.train_loader)
|
||||
|
||||
# set loss functions
|
||||
self.adversarial_loss = AdversarialLoss(type=self.config['losses']['GAN_LOSS'])
|
||||
self.adversarial_loss = self.adversarial_loss.to(self.config['device'])
|
||||
self.l1_loss = nn.L1Loss()
|
||||
# self.perc_loss = PerceptualLoss(
|
||||
# layer_weights={'conv3_4': 0.25, 'conv4_4': 0.25, 'conv5_4': 0.5},
|
||||
# use_input_norm=True,
|
||||
# range_norm=True,
|
||||
# criterion='l1'
|
||||
# ).to(self.config['device'])
|
||||
|
||||
if self.config['losses']['perceptual_weight'] > 0:
|
||||
self.perc_loss = LPIPSLoss(use_input_norm=True, range_norm=True).to(self.config['device'])
|
||||
|
||||
# self.flow_comp_loss = FlowCompletionLoss().to(self.config['device'])
|
||||
# self.flow_comp_loss = FlowCompletionLoss(self.config['device'])
|
||||
|
||||
# set raft
|
||||
self.fix_raft = RAFT_bi(device = self.config['device'])
|
||||
self.fix_flow_complete = RecurrentFlowCompleteNet('weights/recurrent_flow_completion.pth')
|
||||
for p in self.fix_flow_complete.parameters():
|
||||
p.requires_grad = False
|
||||
self.fix_flow_complete.to(self.config['device'])
|
||||
self.fix_flow_complete.eval()
|
||||
|
||||
# self.flow_loss = FlowLoss()
|
||||
|
||||
# setup models including generator and discriminator
|
||||
net = importlib.import_module('model.' + config['model']['net'])
|
||||
self.netG = net.InpaintGenerator()
|
||||
# print(self.netG)
|
||||
self.netG = self.netG.to(self.config['device'])
|
||||
if not self.config['model'].get('no_dis', False):
|
||||
if self.config['model'].get('dis_2d', False):
|
||||
self.netD = net.Discriminator_2D(
|
||||
in_channels=3,
|
||||
use_sigmoid=config['losses']['GAN_LOSS'] != 'hinge')
|
||||
else:
|
||||
self.netD = net.Discriminator(
|
||||
in_channels=3,
|
||||
use_sigmoid=config['losses']['GAN_LOSS'] != 'hinge')
|
||||
self.netD = self.netD.to(self.config['device'])
|
||||
|
||||
self.interp_mode = self.config['model']['interp_mode']
|
||||
# setup optimizers and schedulers
|
||||
self.setup_optimizers()
|
||||
self.setup_schedulers()
|
||||
self.load()
|
||||
|
||||
if config['distributed']:
|
||||
self.netG = DDP(self.netG,
|
||||
device_ids=[self.config['local_rank']],
|
||||
output_device=self.config['local_rank'],
|
||||
broadcast_buffers=True,
|
||||
find_unused_parameters=True)
|
||||
if not self.config['model']['no_dis']:
|
||||
self.netD = DDP(self.netD,
|
||||
device_ids=[self.config['local_rank']],
|
||||
output_device=self.config['local_rank'],
|
||||
broadcast_buffers=True,
|
||||
find_unused_parameters=False)
|
||||
|
||||
# set summary writer
|
||||
self.dis_writer = None
|
||||
self.gen_writer = None
|
||||
self.summary = {}
|
||||
if self.config['global_rank'] == 0 or (not config['distributed']):
|
||||
if not self.config['model']['no_dis']:
|
||||
self.dis_writer = SummaryWriter(
|
||||
os.path.join(config['save_dir'], 'dis'))
|
||||
self.gen_writer = SummaryWriter(
|
||||
os.path.join(config['save_dir'], 'gen'))
|
||||
|
||||
def setup_optimizers(self):
|
||||
"""Set up optimizers."""
|
||||
backbone_params = []
|
||||
for name, param in self.netG.named_parameters():
|
||||
if param.requires_grad:
|
||||
backbone_params.append(param)
|
||||
else:
|
||||
print(f'Params {name} will not be optimized.')
|
||||
|
||||
optim_params = [
|
||||
{
|
||||
'params': backbone_params,
|
||||
'lr': self.config['trainer']['lr']
|
||||
},
|
||||
]
|
||||
|
||||
self.optimG = torch.optim.Adam(optim_params,
|
||||
betas=(self.config['trainer']['beta1'],
|
||||
self.config['trainer']['beta2']))
|
||||
|
||||
if not self.config['model']['no_dis']:
|
||||
self.optimD = torch.optim.Adam(
|
||||
self.netD.parameters(),
|
||||
lr=self.config['trainer']['lr'],
|
||||
betas=(self.config['trainer']['beta1'],
|
||||
self.config['trainer']['beta2']))
|
||||
|
||||
def setup_schedulers(self):
|
||||
"""Set up schedulers."""
|
||||
scheduler_opt = self.config['trainer']['scheduler']
|
||||
scheduler_type = scheduler_opt.pop('type')
|
||||
|
||||
if scheduler_type in ['MultiStepLR', 'MultiStepRestartLR']:
|
||||
self.scheG = MultiStepRestartLR(
|
||||
self.optimG,
|
||||
milestones=scheduler_opt['milestones'],
|
||||
gamma=scheduler_opt['gamma'])
|
||||
if not self.config['model']['no_dis']:
|
||||
self.scheD = MultiStepRestartLR(
|
||||
self.optimD,
|
||||
milestones=scheduler_opt['milestones'],
|
||||
gamma=scheduler_opt['gamma'])
|
||||
elif scheduler_type == 'CosineAnnealingRestartLR':
|
||||
self.scheG = CosineAnnealingRestartLR(
|
||||
self.optimG,
|
||||
periods=scheduler_opt['periods'],
|
||||
restart_weights=scheduler_opt['restart_weights'],
|
||||
eta_min=scheduler_opt['eta_min'])
|
||||
if not self.config['model']['no_dis']:
|
||||
self.scheD = CosineAnnealingRestartLR(
|
||||
self.optimD,
|
||||
periods=scheduler_opt['periods'],
|
||||
restart_weights=scheduler_opt['restart_weights'],
|
||||
eta_min=scheduler_opt['eta_min'])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Scheduler {scheduler_type} is not implemented yet.')
|
||||
|
||||
def update_learning_rate(self):
|
||||
"""Update learning rate."""
|
||||
self.scheG.step()
|
||||
if not self.config['model']['no_dis']:
|
||||
self.scheD.step()
|
||||
|
||||
def get_lr(self):
|
||||
"""Get current learning rate."""
|
||||
return self.optimG.param_groups[0]['lr']
|
||||
|
||||
def add_summary(self, writer, name, val):
|
||||
"""Add tensorboard summary."""
|
||||
if name not in self.summary:
|
||||
self.summary[name] = 0
|
||||
self.summary[name] += val
|
||||
n = self.train_args['log_freq']
|
||||
if writer is not None and self.iteration % n == 0:
|
||||
writer.add_scalar(name, self.summary[name] / n, self.iteration)
|
||||
self.summary[name] = 0
|
||||
|
||||
def load(self):
|
||||
"""Load netG (and netD)."""
|
||||
# get the latest checkpoint
|
||||
model_path = self.config['save_dir']
|
||||
# TODO: add resume name
|
||||
if os.path.isfile(os.path.join(model_path, 'latest.ckpt')):
|
||||
latest_epoch = open(os.path.join(model_path, 'latest.ckpt'),
|
||||
'r').read().splitlines()[-1]
|
||||
else:
|
||||
ckpts = [
|
||||
os.path.basename(i).split('.pth')[0]
|
||||
for i in glob.glob(os.path.join(model_path, '*.pth'))
|
||||
]
|
||||
ckpts.sort()
|
||||
latest_epoch = ckpts[-1][4:] if len(ckpts) > 0 else None
|
||||
|
||||
if latest_epoch is not None:
|
||||
gen_path = os.path.join(model_path,
|
||||
f'gen_{int(latest_epoch):06d}.pth')
|
||||
dis_path = os.path.join(model_path,
|
||||
f'dis_{int(latest_epoch):06d}.pth')
|
||||
opt_path = os.path.join(model_path,
|
||||
f'opt_{int(latest_epoch):06d}.pth')
|
||||
|
||||
if self.config['global_rank'] == 0:
|
||||
print(f'Loading model from {gen_path}...')
|
||||
dataG = torch.load(gen_path, map_location=self.config['device'])
|
||||
self.netG.load_state_dict(dataG)
|
||||
if not self.config['model']['no_dis'] and self.config['model']['load_d']:
|
||||
dataD = torch.load(dis_path, map_location=self.config['device'])
|
||||
self.netD.load_state_dict(dataD)
|
||||
|
||||
data_opt = torch.load(opt_path, map_location=self.config['device'])
|
||||
self.optimG.load_state_dict(data_opt['optimG'])
|
||||
# self.scheG.load_state_dict(data_opt['scheG'])
|
||||
if not self.config['model']['no_dis'] and self.config['model']['load_d']:
|
||||
self.optimD.load_state_dict(data_opt['optimD'])
|
||||
# self.scheD.load_state_dict(data_opt['scheD'])
|
||||
self.epoch = data_opt['epoch']
|
||||
self.iteration = data_opt['iteration']
|
||||
else:
|
||||
gen_path = self.config['trainer'].get('gen_path', None)
|
||||
dis_path = self.config['trainer'].get('dis_path', None)
|
||||
opt_path = self.config['trainer'].get('opt_path', None)
|
||||
if gen_path is not None:
|
||||
if self.config['global_rank'] == 0:
|
||||
print(f'Loading Gen-Net from {gen_path}...')
|
||||
dataG = torch.load(gen_path, map_location=self.config['device'])
|
||||
self.netG.load_state_dict(dataG)
|
||||
|
||||
if dis_path is not None and not self.config['model']['no_dis'] and self.config['model']['load_d']:
|
||||
if self.config['global_rank'] == 0:
|
||||
print(f'Loading Dis-Net from {dis_path}...')
|
||||
dataD = torch.load(dis_path, map_location=self.config['device'])
|
||||
self.netD.load_state_dict(dataD)
|
||||
if opt_path is not None:
|
||||
data_opt = torch.load(opt_path, map_location=self.config['device'])
|
||||
self.optimG.load_state_dict(data_opt['optimG'])
|
||||
self.scheG.load_state_dict(data_opt['scheG'])
|
||||
if not self.config['model']['no_dis'] and self.config['model']['load_d']:
|
||||
self.optimD.load_state_dict(data_opt['optimD'])
|
||||
self.scheD.load_state_dict(data_opt['scheD'])
|
||||
else:
|
||||
if self.config['global_rank'] == 0:
|
||||
print('Warnning: There is no trained model found.'
|
||||
'An initialized model will be used.')
|
||||
|
||||
def save(self, it):
|
||||
"""Save parameters every eval_epoch"""
|
||||
if self.config['global_rank'] == 0:
|
||||
# configure path
|
||||
gen_path = os.path.join(self.config['save_dir'],
|
||||
f'gen_{it:06d}.pth')
|
||||
dis_path = os.path.join(self.config['save_dir'],
|
||||
f'dis_{it:06d}.pth')
|
||||
opt_path = os.path.join(self.config['save_dir'],
|
||||
f'opt_{it:06d}.pth')
|
||||
print(f'\nsaving model to {gen_path} ...')
|
||||
|
||||
# remove .module for saving
|
||||
if isinstance(self.netG, torch.nn.DataParallel) or isinstance(self.netG, DDP):
|
||||
netG = self.netG.module
|
||||
if not self.config['model']['no_dis']:
|
||||
netD = self.netD.module
|
||||
else:
|
||||
netG = self.netG
|
||||
if not self.config['model']['no_dis']:
|
||||
netD = self.netD
|
||||
|
||||
# save checkpoints
|
||||
torch.save(netG.state_dict(), gen_path)
|
||||
if not self.config['model']['no_dis']:
|
||||
torch.save(netD.state_dict(), dis_path)
|
||||
torch.save(
|
||||
{
|
||||
'epoch': self.epoch,
|
||||
'iteration': self.iteration,
|
||||
'optimG': self.optimG.state_dict(),
|
||||
'optimD': self.optimD.state_dict(),
|
||||
'scheG': self.scheG.state_dict(),
|
||||
'scheD': self.scheD.state_dict()
|
||||
}, opt_path)
|
||||
else:
|
||||
torch.save(
|
||||
{
|
||||
'epoch': self.epoch,
|
||||
'iteration': self.iteration,
|
||||
'optimG': self.optimG.state_dict(),
|
||||
'scheG': self.scheG.state_dict()
|
||||
}, opt_path)
|
||||
|
||||
latest_path = os.path.join(self.config['save_dir'], 'latest.ckpt')
|
||||
os.system(f"echo {it:06d} > {latest_path}")
|
||||
|
||||
def train(self):
|
||||
"""training entry"""
|
||||
pbar = range(int(self.train_args['iterations']))
|
||||
if self.config['global_rank'] == 0:
|
||||
pbar = tqdm(pbar,
|
||||
initial=self.iteration,
|
||||
dynamic_ncols=True,
|
||||
smoothing=0.01)
|
||||
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(filename)s[line:%(lineno)d]"
|
||||
"%(levelname)s %(message)s",
|
||||
datefmt="%a, %d %b %Y %H:%M:%S",
|
||||
filename=f"logs/{self.config['save_dir'].split('/')[-1]}.log",
|
||||
filemode='w')
|
||||
|
||||
while True:
|
||||
self.epoch += 1
|
||||
self.prefetcher.reset()
|
||||
if self.config['distributed']:
|
||||
self.train_sampler.set_epoch(self.epoch)
|
||||
self._train_epoch(pbar)
|
||||
if self.iteration > self.train_args['iterations']:
|
||||
break
|
||||
print('\nEnd training....')
|
||||
|
||||
def _train_epoch(self, pbar):
|
||||
"""Process input and calculate loss every training epoch"""
|
||||
device = self.config['device']
|
||||
train_data = self.prefetcher.next()
|
||||
while train_data is not None:
|
||||
self.iteration += 1
|
||||
frames, masks, flows_f, flows_b, _ = train_data
|
||||
frames, masks = frames.to(device), masks.to(device).float()
|
||||
l_t = self.num_local_frames
|
||||
b, t, c, h, w = frames.size()
|
||||
gt_local_frames = frames[:, :l_t, ...]
|
||||
local_masks = masks[:, :l_t, ...].contiguous()
|
||||
|
||||
masked_frames = frames * (1 - masks)
|
||||
masked_local_frames = masked_frames[:, :l_t, ...]
|
||||
# get gt optical flow
|
||||
if flows_f[0] == 'None' or flows_b[0] == 'None':
|
||||
gt_flows_bi = self.fix_raft(gt_local_frames)
|
||||
else:
|
||||
gt_flows_bi = (flows_f.to(device), flows_b.to(device))
|
||||
|
||||
# ---- complete flow ----
|
||||
pred_flows_bi, _ = self.fix_flow_complete.forward_bidirect_flow(gt_flows_bi, local_masks)
|
||||
pred_flows_bi = self.fix_flow_complete.combine_flow(gt_flows_bi, pred_flows_bi, local_masks)
|
||||
# pred_flows_bi = gt_flows_bi
|
||||
|
||||
# ---- image propagation ----
|
||||
prop_imgs, updated_local_masks = self.netG.module.img_propagation(masked_local_frames, pred_flows_bi, local_masks, interpolation=self.interp_mode)
|
||||
updated_masks = masks.clone()
|
||||
updated_masks[:, :l_t, ...] = updated_local_masks.view(b, l_t, 1, h, w)
|
||||
updated_frames = masked_frames.clone()
|
||||
prop_local_frames = gt_local_frames * (1-local_masks) + prop_imgs.view(b, l_t, 3, h, w) * local_masks # merge
|
||||
updated_frames[:, :l_t, ...] = prop_local_frames
|
||||
|
||||
# ---- feature propagation + Transformer ----
|
||||
pred_imgs = self.netG(updated_frames, pred_flows_bi, masks, updated_masks, l_t)
|
||||
pred_imgs = pred_imgs.view(b, -1, c, h, w)
|
||||
|
||||
# get the local frames
|
||||
pred_local_frames = pred_imgs[:, :l_t, ...]
|
||||
comp_local_frames = gt_local_frames * (1. - local_masks) + pred_local_frames * local_masks
|
||||
comp_imgs = frames * (1. - masks) + pred_imgs * masks
|
||||
|
||||
gen_loss = 0
|
||||
dis_loss = 0
|
||||
# optimize net_g
|
||||
if not self.config['model']['no_dis']:
|
||||
for p in self.netD.parameters():
|
||||
p.requires_grad = False
|
||||
|
||||
self.optimG.zero_grad()
|
||||
|
||||
# generator l1 loss
|
||||
hole_loss = self.l1_loss(pred_imgs * masks, frames * masks)
|
||||
hole_loss = hole_loss / torch.mean(masks) * self.config['losses']['hole_weight']
|
||||
gen_loss += hole_loss
|
||||
self.add_summary(self.gen_writer, 'loss/hole_loss', hole_loss.item())
|
||||
|
||||
valid_loss = self.l1_loss(pred_imgs * (1 - masks), frames * (1 - masks))
|
||||
valid_loss = valid_loss / torch.mean(1-masks) * self.config['losses']['valid_weight']
|
||||
gen_loss += valid_loss
|
||||
self.add_summary(self.gen_writer, 'loss/valid_loss', valid_loss.item())
|
||||
|
||||
# perceptual loss
|
||||
if self.config['losses']['perceptual_weight'] > 0:
|
||||
perc_loss = self.perc_loss(pred_imgs.view(-1,3,h,w), frames.view(-1,3,h,w))[0] * self.config['losses']['perceptual_weight']
|
||||
gen_loss += perc_loss
|
||||
self.add_summary(self.gen_writer, 'loss/perc_loss', perc_loss.item())
|
||||
|
||||
# gan loss
|
||||
if not self.config['model']['no_dis']:
|
||||
# generator adversarial loss
|
||||
gen_clip = self.netD(comp_imgs)
|
||||
gan_loss = self.adversarial_loss(gen_clip, True, False)
|
||||
gan_loss = gan_loss * self.config['losses']['adversarial_weight']
|
||||
gen_loss += gan_loss
|
||||
self.add_summary(self.gen_writer, 'loss/gan_loss', gan_loss.item())
|
||||
gen_loss.backward()
|
||||
self.optimG.step()
|
||||
|
||||
if not self.config['model']['no_dis']:
|
||||
# optimize net_d
|
||||
for p in self.netD.parameters():
|
||||
p.requires_grad = True
|
||||
self.optimD.zero_grad()
|
||||
|
||||
# discriminator adversarial loss
|
||||
real_clip = self.netD(frames)
|
||||
fake_clip = self.netD(comp_imgs.detach())
|
||||
dis_real_loss = self.adversarial_loss(real_clip, True, True)
|
||||
dis_fake_loss = self.adversarial_loss(fake_clip, False, True)
|
||||
dis_loss += (dis_real_loss + dis_fake_loss) / 2
|
||||
self.add_summary(self.dis_writer, 'loss/dis_vid_real', dis_real_loss.item())
|
||||
self.add_summary(self.dis_writer, 'loss/dis_vid_fake', dis_fake_loss.item())
|
||||
dis_loss.backward()
|
||||
self.optimD.step()
|
||||
|
||||
self.update_learning_rate()
|
||||
|
||||
# write image to tensorboard
|
||||
if self.iteration % 200 == 0:
|
||||
# img to cpu
|
||||
t = 0
|
||||
gt_local_frames_cpu = ((gt_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
|
||||
masked_local_frames = ((masked_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
|
||||
prop_local_frames_cpu = ((prop_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
|
||||
pred_local_frames_cpu = ((pred_local_frames.view(b,-1,3,h,w) + 1)/2.0).cpu()
|
||||
img_results = torch.cat([masked_local_frames[0][t], gt_local_frames_cpu[0][t],
|
||||
prop_local_frames_cpu[0][t], pred_local_frames_cpu[0][t]], 1)
|
||||
img_results = torchvision.utils.make_grid(img_results, nrow=1, normalize=True)
|
||||
if self.gen_writer is not None:
|
||||
self.gen_writer.add_image(f'img/img:inp-gt-res-{t}', img_results, self.iteration)
|
||||
|
||||
t = 5
|
||||
if masked_local_frames.shape[1] > 5:
|
||||
img_results = torch.cat([masked_local_frames[0][t], gt_local_frames_cpu[0][t],
|
||||
prop_local_frames_cpu[0][t], pred_local_frames_cpu[0][t]], 1)
|
||||
img_results = torchvision.utils.make_grid(img_results, nrow=1, normalize=True)
|
||||
if self.gen_writer is not None:
|
||||
self.gen_writer.add_image(f'img/img:inp-gt-res-{t}', img_results, self.iteration)
|
||||
|
||||
# flow to cpu
|
||||
gt_flows_forward_cpu = flow_to_image(gt_flows_bi[0][0]).cpu()
|
||||
masked_flows_forward_cpu = (gt_flows_forward_cpu[0] * (1-local_masks[0][0].cpu())).to(gt_flows_forward_cpu)
|
||||
pred_flows_forward_cpu = flow_to_image(pred_flows_bi[0][0]).cpu()
|
||||
|
||||
flow_results = torch.cat([gt_flows_forward_cpu[0], masked_flows_forward_cpu, pred_flows_forward_cpu[0]], 1)
|
||||
if self.gen_writer is not None:
|
||||
self.gen_writer.add_image('img/flow:gt-pred', flow_results, self.iteration)
|
||||
|
||||
# console logs
|
||||
if self.config['global_rank'] == 0:
|
||||
pbar.update(1)
|
||||
if not self.config['model']['no_dis']:
|
||||
pbar.set_description((f"d: {dis_loss.item():.3f}; "
|
||||
f"hole: {hole_loss.item():.3f}; "
|
||||
f"valid: {valid_loss.item():.3f}"))
|
||||
else:
|
||||
pbar.set_description((f"hole: {hole_loss.item():.3f}; "
|
||||
f"valid: {valid_loss.item():.3f}"))
|
||||
|
||||
if self.iteration % self.train_args['log_freq'] == 0:
|
||||
if not self.config['model']['no_dis']:
|
||||
logging.info(f"[Iter {self.iteration}] "
|
||||
f"d: {dis_loss.item():.4f}; "
|
||||
f"hole: {hole_loss.item():.4f}; "
|
||||
f"valid: {valid_loss.item():.4f}")
|
||||
else:
|
||||
logging.info(f"[Iter {self.iteration}] "
|
||||
f"hole: {hole_loss.item():.4f}; "
|
||||
f"valid: {valid_loss.item():.4f}")
|
||||
|
||||
# saving models
|
||||
if self.iteration % self.train_args['save_freq'] == 0:
|
||||
self.save(int(self.iteration))
|
||||
|
||||
if self.iteration > self.train_args['iterations']:
|
||||
break
|
||||
|
||||
train_data = self.prefetcher.next()
|
||||
@@ -0,0 +1,380 @@
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
import importlib
|
||||
from tqdm import tqdm
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from core.prefetch_dataloader import PrefetchDataLoader, CPUPrefetcher
|
||||
from torch.utils.data.distributed import DistributedSampler
|
||||
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
|
||||
from core.lr_scheduler import MultiStepRestartLR, CosineAnnealingRestartLR
|
||||
from core.dataset import TrainDataset
|
||||
|
||||
from model.modules.flow_comp_raft import RAFT_bi, FlowLoss, EdgeLoss
|
||||
|
||||
# from skimage.feature import canny
|
||||
from model.canny.canny_filter import Canny
|
||||
from RAFT.utils.flow_viz_pt import flow_to_image
|
||||
|
||||
|
||||
class Trainer:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.epoch = 0
|
||||
self.iteration = 0
|
||||
self.num_local_frames = config['train_data_loader']['num_local_frames']
|
||||
self.num_ref_frames = config['train_data_loader']['num_ref_frames']
|
||||
|
||||
# setup data set and data loader
|
||||
self.train_dataset = TrainDataset(config['train_data_loader'])
|
||||
|
||||
self.train_sampler = None
|
||||
self.train_args = config['trainer']
|
||||
if config['distributed']:
|
||||
self.train_sampler = DistributedSampler(
|
||||
self.train_dataset,
|
||||
num_replicas=config['world_size'],
|
||||
rank=config['global_rank'])
|
||||
|
||||
dataloader_args = dict(
|
||||
dataset=self.train_dataset,
|
||||
batch_size=self.train_args['batch_size'] // config['world_size'],
|
||||
shuffle=(self.train_sampler is None),
|
||||
num_workers=self.train_args['num_workers'],
|
||||
sampler=self.train_sampler,
|
||||
drop_last=True)
|
||||
|
||||
self.train_loader = PrefetchDataLoader(self.train_args['num_prefetch_queue'], **dataloader_args)
|
||||
self.prefetcher = CPUPrefetcher(self.train_loader)
|
||||
|
||||
# set raft
|
||||
self.fix_raft = RAFT_bi(device = self.config['device'])
|
||||
self.flow_loss = FlowLoss()
|
||||
self.edge_loss = EdgeLoss()
|
||||
self.canny = Canny(sigma=(2,2), low_threshold=0.1, high_threshold=0.2)
|
||||
|
||||
# setup models including generator and discriminator
|
||||
net = importlib.import_module('model.' + config['model']['net'])
|
||||
self.netG = net.RecurrentFlowCompleteNet()
|
||||
# print(self.netG)
|
||||
self.netG = self.netG.to(self.config['device'])
|
||||
|
||||
# setup optimizers and schedulers
|
||||
self.setup_optimizers()
|
||||
self.setup_schedulers()
|
||||
self.load()
|
||||
|
||||
if config['distributed']:
|
||||
self.netG = DDP(self.netG,
|
||||
device_ids=[self.config['local_rank']],
|
||||
output_device=self.config['local_rank'],
|
||||
broadcast_buffers=True,
|
||||
find_unused_parameters=True)
|
||||
|
||||
# set summary writer
|
||||
self.dis_writer = None
|
||||
self.gen_writer = None
|
||||
self.summary = {}
|
||||
if self.config['global_rank'] == 0 or (not config['distributed']):
|
||||
self.gen_writer = SummaryWriter(
|
||||
os.path.join(config['save_dir'], 'gen'))
|
||||
|
||||
def setup_optimizers(self):
|
||||
"""Set up optimizers."""
|
||||
backbone_params = []
|
||||
for name, param in self.netG.named_parameters():
|
||||
if param.requires_grad:
|
||||
backbone_params.append(param)
|
||||
else:
|
||||
print(f'Params {name} will not be optimized.')
|
||||
|
||||
optim_params = [
|
||||
{
|
||||
'params': backbone_params,
|
||||
'lr': self.config['trainer']['lr']
|
||||
},
|
||||
]
|
||||
|
||||
self.optimG = torch.optim.Adam(optim_params,
|
||||
betas=(self.config['trainer']['beta1'],
|
||||
self.config['trainer']['beta2']))
|
||||
|
||||
|
||||
def setup_schedulers(self):
|
||||
"""Set up schedulers."""
|
||||
scheduler_opt = self.config['trainer']['scheduler']
|
||||
scheduler_type = scheduler_opt.pop('type')
|
||||
|
||||
if scheduler_type in ['MultiStepLR', 'MultiStepRestartLR']:
|
||||
self.scheG = MultiStepRestartLR(
|
||||
self.optimG,
|
||||
milestones=scheduler_opt['milestones'],
|
||||
gamma=scheduler_opt['gamma'])
|
||||
elif scheduler_type == 'CosineAnnealingRestartLR':
|
||||
self.scheG = CosineAnnealingRestartLR(
|
||||
self.optimG,
|
||||
periods=scheduler_opt['periods'],
|
||||
restart_weights=scheduler_opt['restart_weights'])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Scheduler {scheduler_type} is not implemented yet.')
|
||||
|
||||
def update_learning_rate(self):
|
||||
"""Update learning rate."""
|
||||
self.scheG.step()
|
||||
|
||||
def get_lr(self):
|
||||
"""Get current learning rate."""
|
||||
return self.optimG.param_groups[0]['lr']
|
||||
|
||||
def add_summary(self, writer, name, val):
|
||||
"""Add tensorboard summary."""
|
||||
if name not in self.summary:
|
||||
self.summary[name] = 0
|
||||
self.summary[name] += val
|
||||
n = self.train_args['log_freq']
|
||||
if writer is not None and self.iteration % n == 0:
|
||||
writer.add_scalar(name, self.summary[name] / n, self.iteration)
|
||||
self.summary[name] = 0
|
||||
|
||||
def load(self):
|
||||
"""Load netG."""
|
||||
# get the latest checkpoint
|
||||
model_path = self.config['save_dir']
|
||||
if os.path.isfile(os.path.join(model_path, 'latest.ckpt')):
|
||||
latest_epoch = open(os.path.join(model_path, 'latest.ckpt'),
|
||||
'r').read().splitlines()[-1]
|
||||
else:
|
||||
ckpts = [
|
||||
os.path.basename(i).split('.pth')[0]
|
||||
for i in glob.glob(os.path.join(model_path, '*.pth'))
|
||||
]
|
||||
ckpts.sort()
|
||||
latest_epoch = ckpts[-1][4:] if len(ckpts) > 0 else None
|
||||
|
||||
if latest_epoch is not None:
|
||||
gen_path = os.path.join(model_path, f'gen_{int(latest_epoch):06d}.pth')
|
||||
opt_path = os.path.join(model_path,f'opt_{int(latest_epoch):06d}.pth')
|
||||
|
||||
if self.config['global_rank'] == 0:
|
||||
print(f'Loading model from {gen_path}...')
|
||||
dataG = torch.load(gen_path, map_location=self.config['device'])
|
||||
self.netG.load_state_dict(dataG)
|
||||
|
||||
|
||||
data_opt = torch.load(opt_path, map_location=self.config['device'])
|
||||
self.optimG.load_state_dict(data_opt['optimG'])
|
||||
self.scheG.load_state_dict(data_opt['scheG'])
|
||||
|
||||
self.epoch = data_opt['epoch']
|
||||
self.iteration = data_opt['iteration']
|
||||
|
||||
else:
|
||||
if self.config['global_rank'] == 0:
|
||||
print('Warnning: There is no trained model found.'
|
||||
'An initialized model will be used.')
|
||||
|
||||
def save(self, it):
|
||||
"""Save parameters every eval_epoch"""
|
||||
if self.config['global_rank'] == 0:
|
||||
# configure path
|
||||
gen_path = os.path.join(self.config['save_dir'],
|
||||
f'gen_{it:06d}.pth')
|
||||
opt_path = os.path.join(self.config['save_dir'],
|
||||
f'opt_{it:06d}.pth')
|
||||
print(f'\nsaving model to {gen_path} ...')
|
||||
|
||||
# remove .module for saving
|
||||
if isinstance(self.netG, torch.nn.DataParallel) or isinstance(self.netG, DDP):
|
||||
netG = self.netG.module
|
||||
else:
|
||||
netG = self.netG
|
||||
|
||||
# save checkpoints
|
||||
torch.save(netG.state_dict(), gen_path)
|
||||
torch.save(
|
||||
{
|
||||
'epoch': self.epoch,
|
||||
'iteration': self.iteration,
|
||||
'optimG': self.optimG.state_dict(),
|
||||
'scheG': self.scheG.state_dict()
|
||||
}, opt_path)
|
||||
|
||||
latest_path = os.path.join(self.config['save_dir'], 'latest.ckpt')
|
||||
os.system(f"echo {it:06d} > {latest_path}")
|
||||
|
||||
def train(self):
|
||||
"""training entry"""
|
||||
pbar = range(int(self.train_args['iterations']))
|
||||
if self.config['global_rank'] == 0:
|
||||
pbar = tqdm(pbar,
|
||||
initial=self.iteration,
|
||||
dynamic_ncols=True,
|
||||
smoothing=0.01)
|
||||
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(filename)s[line:%(lineno)d]"
|
||||
"%(levelname)s %(message)s",
|
||||
datefmt="%a, %d %b %Y %H:%M:%S",
|
||||
filename=f"logs/{self.config['save_dir'].split('/')[-1]}.log",
|
||||
filemode='w')
|
||||
|
||||
while True:
|
||||
self.epoch += 1
|
||||
self.prefetcher.reset()
|
||||
if self.config['distributed']:
|
||||
self.train_sampler.set_epoch(self.epoch)
|
||||
self._train_epoch(pbar)
|
||||
if self.iteration > self.train_args['iterations']:
|
||||
break
|
||||
print('\nEnd training....')
|
||||
|
||||
# def get_edges(self, flows): # fgvc
|
||||
# # (b, t, 2, H, W)
|
||||
# b, t, _, h, w = flows.shape
|
||||
# flows = flows.view(-1, 2, h, w)
|
||||
# flows_list = flows.permute(0, 2, 3, 1).cpu().numpy()
|
||||
# edges = []
|
||||
# for f in list(flows_list):
|
||||
# flows_gray = (f[:, :, 0] ** 2 + f[:, :, 1] ** 2) ** 0.5
|
||||
# if flows_gray.max() < 1:
|
||||
# flows_gray = flows_gray*0
|
||||
# else:
|
||||
# flows_gray = flows_gray / flows_gray.max()
|
||||
|
||||
# edge = canny(flows_gray, sigma=2, low_threshold=0.1, high_threshold=0.2) # fgvc
|
||||
# edge = torch.from_numpy(edge).view(1, 1, h, w).float()
|
||||
# edges.append(edge)
|
||||
# edges = torch.stack(edges, dim=0).to(self.config['device'])
|
||||
# edges = edges.view(b, t, 1, h, w)
|
||||
# return edges
|
||||
|
||||
def get_edges(self, flows):
|
||||
# (b, t, 2, H, W)
|
||||
b, t, _, h, w = flows.shape
|
||||
flows = flows.view(-1, 2, h, w)
|
||||
flows_gray = (flows[:, 0, None] ** 2 + flows[:, 1, None] ** 2) ** 0.5
|
||||
if flows_gray.max() < 1:
|
||||
flows_gray = flows_gray*0
|
||||
else:
|
||||
flows_gray = flows_gray / flows_gray.max()
|
||||
|
||||
magnitude, edges = self.canny(flows_gray.float())
|
||||
edges = edges.view(b, t, 1, h, w)
|
||||
return edges
|
||||
|
||||
def _train_epoch(self, pbar):
|
||||
"""Process input and calculate loss every training epoch"""
|
||||
device = self.config['device']
|
||||
train_data = self.prefetcher.next()
|
||||
while train_data is not None:
|
||||
self.iteration += 1
|
||||
frames, masks, flows_f, flows_b, _ = train_data
|
||||
frames, masks = frames.to(device), masks.to(device)
|
||||
masks = masks.float()
|
||||
|
||||
l_t = self.num_local_frames
|
||||
b, t, c, h, w = frames.size()
|
||||
gt_local_frames = frames[:, :l_t, ...]
|
||||
local_masks = masks[:, :l_t, ...].contiguous()
|
||||
|
||||
# get gt optical flow
|
||||
if flows_f[0] == 'None' or flows_b[0] == 'None':
|
||||
gt_flows_bi = self.fix_raft(gt_local_frames)
|
||||
else:
|
||||
gt_flows_bi = (flows_f.to(device), flows_b.to(device))
|
||||
|
||||
# get gt edge
|
||||
gt_edges_forward = self.get_edges(gt_flows_bi[0])
|
||||
gt_edges_backward = self.get_edges(gt_flows_bi[1])
|
||||
gt_edges_bi = [gt_edges_forward, gt_edges_backward]
|
||||
|
||||
# complete flow
|
||||
pred_flows_bi, pred_edges_bi = self.netG.module.forward_bidirect_flow(gt_flows_bi, local_masks)
|
||||
|
||||
# optimize net_g
|
||||
self.optimG.zero_grad()
|
||||
|
||||
# compulte flow_loss
|
||||
flow_loss, warp_loss = self.flow_loss(pred_flows_bi, gt_flows_bi, local_masks, gt_local_frames)
|
||||
flow_loss = flow_loss * self.config['losses']['flow_weight']
|
||||
warp_loss = warp_loss * 0.01
|
||||
self.add_summary(self.gen_writer, 'loss/flow_loss', flow_loss.item())
|
||||
self.add_summary(self.gen_writer, 'loss/warp_loss', warp_loss.item())
|
||||
|
||||
# compute edge loss
|
||||
edge_loss = self.edge_loss(pred_edges_bi, gt_edges_bi, local_masks)
|
||||
edge_loss = edge_loss*1.0
|
||||
self.add_summary(self.gen_writer, 'loss/edge_loss', edge_loss.item())
|
||||
|
||||
loss = flow_loss + warp_loss + edge_loss
|
||||
loss.backward()
|
||||
self.optimG.step()
|
||||
self.update_learning_rate()
|
||||
|
||||
# write image to tensorboard
|
||||
# if self.iteration % 200 == 0:
|
||||
if self.iteration % 200 == 0 and self.gen_writer is not None:
|
||||
t = 5
|
||||
# forward to cpu
|
||||
gt_flows_forward_cpu = flow_to_image(gt_flows_bi[0][0]).cpu()
|
||||
masked_flows_forward_cpu = (gt_flows_forward_cpu[t] * (1-local_masks[0][t].cpu())).to(gt_flows_forward_cpu)
|
||||
pred_flows_forward_cpu = flow_to_image(pred_flows_bi[0][0]).cpu()
|
||||
|
||||
flow_results = torch.cat([gt_flows_forward_cpu[t], masked_flows_forward_cpu, pred_flows_forward_cpu[t]], 1)
|
||||
self.gen_writer.add_image('img/flow-f:gt-pred', flow_results, self.iteration)
|
||||
|
||||
# backward to cpu
|
||||
gt_flows_backward_cpu = flow_to_image(gt_flows_bi[1][0]).cpu()
|
||||
masked_flows_backward_cpu = (gt_flows_backward_cpu[t] * (1-local_masks[0][t+1].cpu())).to(gt_flows_backward_cpu)
|
||||
pred_flows_backward_cpu = flow_to_image(pred_flows_bi[1][0]).cpu()
|
||||
|
||||
flow_results = torch.cat([gt_flows_backward_cpu[t], masked_flows_backward_cpu, pred_flows_backward_cpu[t]], 1)
|
||||
self.gen_writer.add_image('img/flow-b:gt-pred', flow_results, self.iteration)
|
||||
|
||||
# TODO: show edge
|
||||
# forward
|
||||
gt_edges_forward_cpu = gt_edges_bi[0][0].cpu()
|
||||
masked_edges_forward_cpu = (gt_edges_forward_cpu[t] * (1-local_masks[0][t].cpu())).to(gt_edges_forward_cpu)
|
||||
pred_edges_forward_cpu = pred_edges_bi[0][0].cpu()
|
||||
|
||||
edge_results = torch.cat([gt_edges_forward_cpu[t], masked_edges_forward_cpu, pred_edges_forward_cpu[t]], 1)
|
||||
self.gen_writer.add_image('img/edge-f:gt-pred', edge_results, self.iteration)
|
||||
# backward
|
||||
gt_edges_backward_cpu = gt_edges_bi[1][0].cpu()
|
||||
masked_edges_backward_cpu = (gt_edges_backward_cpu[t] * (1-local_masks[0][t+1].cpu())).to(gt_edges_backward_cpu)
|
||||
pred_edges_backward_cpu = pred_edges_bi[1][0].cpu()
|
||||
|
||||
edge_results = torch.cat([gt_edges_backward_cpu[t], masked_edges_backward_cpu, pred_edges_backward_cpu[t]], 1)
|
||||
self.gen_writer.add_image('img/edge-b:gt-pred', edge_results, self.iteration)
|
||||
|
||||
# console logs
|
||||
if self.config['global_rank'] == 0:
|
||||
pbar.update(1)
|
||||
pbar.set_description((f"flow: {flow_loss.item():.3f}; "
|
||||
f"warp: {warp_loss.item():.3f}; "
|
||||
f"edge: {edge_loss.item():.3f}; "
|
||||
f"lr: {self.get_lr()}"))
|
||||
|
||||
if self.iteration % self.train_args['log_freq'] == 0:
|
||||
logging.info(f"[Iter {self.iteration}] "
|
||||
f"flow: {flow_loss.item():.4f}; "
|
||||
f"warp: {warp_loss.item():.4f}")
|
||||
|
||||
# saving models
|
||||
if self.iteration % self.train_args['save_freq'] == 0:
|
||||
self.save(int(self.iteration))
|
||||
|
||||
if self.iteration > self.train_args['iterations']:
|
||||
break
|
||||
|
||||
train_data = self.prefetcher.next()
|
||||
@@ -0,0 +1,371 @@
|
||||
import os
|
||||
import io
|
||||
import cv2
|
||||
import random
|
||||
import numpy as np
|
||||
from PIL import Image, ImageOps
|
||||
import zipfile
|
||||
import math
|
||||
|
||||
import torch
|
||||
import matplotlib
|
||||
import matplotlib.patches as patches
|
||||
from matplotlib.path import Path
|
||||
from matplotlib import pyplot as plt
|
||||
from torchvision import transforms
|
||||
|
||||
# matplotlib.use('agg')
|
||||
|
||||
# ###########################################################################
|
||||
# Directory IO
|
||||
# ###########################################################################
|
||||
|
||||
|
||||
def read_dirnames_under_root(root_dir):
|
||||
dirnames = [
|
||||
name for i, name in enumerate(sorted(os.listdir(root_dir)))
|
||||
if os.path.isdir(os.path.join(root_dir, name))
|
||||
]
|
||||
print(f'Reading directories under {root_dir}, num: {len(dirnames)}')
|
||||
return dirnames
|
||||
|
||||
|
||||
class TrainZipReader(object):
|
||||
file_dict = dict()
|
||||
|
||||
def __init__(self):
|
||||
super(TrainZipReader, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def build_file_dict(path):
|
||||
file_dict = TrainZipReader.file_dict
|
||||
if path in file_dict:
|
||||
return file_dict[path]
|
||||
else:
|
||||
file_handle = zipfile.ZipFile(path, 'r')
|
||||
file_dict[path] = file_handle
|
||||
return file_dict[path]
|
||||
|
||||
@staticmethod
|
||||
def imread(path, idx):
|
||||
zfile = TrainZipReader.build_file_dict(path)
|
||||
filelist = zfile.namelist()
|
||||
filelist.sort()
|
||||
data = zfile.read(filelist[idx])
|
||||
#
|
||||
im = Image.open(io.BytesIO(data))
|
||||
return im
|
||||
|
||||
|
||||
class TestZipReader(object):
|
||||
file_dict = dict()
|
||||
|
||||
def __init__(self):
|
||||
super(TestZipReader, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def build_file_dict(path):
|
||||
file_dict = TestZipReader.file_dict
|
||||
if path in file_dict:
|
||||
return file_dict[path]
|
||||
else:
|
||||
file_handle = zipfile.ZipFile(path, 'r')
|
||||
file_dict[path] = file_handle
|
||||
return file_dict[path]
|
||||
|
||||
@staticmethod
|
||||
def imread(path, idx):
|
||||
zfile = TestZipReader.build_file_dict(path)
|
||||
filelist = zfile.namelist()
|
||||
filelist.sort()
|
||||
data = zfile.read(filelist[idx])
|
||||
file_bytes = np.asarray(bytearray(data), dtype=np.uint8)
|
||||
im = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
|
||||
im = Image.fromarray(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))
|
||||
# im = Image.open(io.BytesIO(data))
|
||||
return im
|
||||
|
||||
|
||||
# ###########################################################################
|
||||
# Data augmentation
|
||||
# ###########################################################################
|
||||
|
||||
|
||||
def to_tensors():
|
||||
return transforms.Compose([Stack(), ToTorchFormatTensor()])
|
||||
|
||||
|
||||
class GroupRandomHorizontalFlowFlip(object):
|
||||
"""Randomly horizontally flips the given PIL.Image with a probability of 0.5
|
||||
"""
|
||||
def __call__(self, img_group, flowF_group, flowB_group):
|
||||
v = random.random()
|
||||
if v < 0.5:
|
||||
ret_img = [
|
||||
img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group
|
||||
]
|
||||
ret_flowF = [ff[:, ::-1] * [-1.0, 1.0] for ff in flowF_group]
|
||||
ret_flowB = [fb[:, ::-1] * [-1.0, 1.0] for fb in flowB_group]
|
||||
return ret_img, ret_flowF, ret_flowB
|
||||
else:
|
||||
return img_group, flowF_group, flowB_group
|
||||
|
||||
|
||||
class GroupRandomHorizontalFlip(object):
|
||||
"""Randomly horizontally flips the given PIL.Image with a probability of 0.5
|
||||
"""
|
||||
def __call__(self, img_group, is_flow=False):
|
||||
v = random.random()
|
||||
if v < 0.5:
|
||||
ret = [img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group]
|
||||
if is_flow:
|
||||
for i in range(0, len(ret), 2):
|
||||
# invert flow pixel values when flipping
|
||||
ret[i] = ImageOps.invert(ret[i])
|
||||
return ret
|
||||
else:
|
||||
return img_group
|
||||
|
||||
|
||||
class Stack(object):
|
||||
def __init__(self, roll=False):
|
||||
self.roll = roll
|
||||
|
||||
def __call__(self, img_group):
|
||||
mode = img_group[0].mode
|
||||
if mode == '1':
|
||||
img_group = [img.convert('L') for img in img_group]
|
||||
mode = 'L'
|
||||
if mode == 'L':
|
||||
return np.stack([np.expand_dims(x, 2) for x in img_group], axis=2)
|
||||
elif mode == 'RGB':
|
||||
if self.roll:
|
||||
return np.stack([np.array(x)[:, :, ::-1] for x in img_group],
|
||||
axis=2)
|
||||
else:
|
||||
return np.stack(img_group, axis=2)
|
||||
else:
|
||||
raise NotImplementedError(f"Image mode {mode}")
|
||||
|
||||
|
||||
class ToTorchFormatTensor(object):
|
||||
""" Converts a PIL.Image (RGB) or numpy.ndarray (H x W x C) in the range [0, 255]
|
||||
to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0] """
|
||||
def __init__(self, div=True):
|
||||
self.div = div
|
||||
|
||||
def __call__(self, pic):
|
||||
if isinstance(pic, np.ndarray):
|
||||
# numpy img: [L, C, H, W]
|
||||
img = torch.from_numpy(pic).permute(2, 3, 0, 1).contiguous()
|
||||
else:
|
||||
# handle PIL Image
|
||||
img = torch.ByteTensor(torch.ByteStorage.from_buffer(
|
||||
pic.tobytes()))
|
||||
img = img.view(pic.size[1], pic.size[0], len(pic.mode))
|
||||
# put it from HWC to CHW format
|
||||
# yikes, this transpose takes 80% of the loading time/CPU
|
||||
img = img.transpose(0, 1).transpose(0, 2).contiguous()
|
||||
img = img.float().div(255) if self.div else img.float()
|
||||
return img
|
||||
|
||||
|
||||
# ###########################################################################
|
||||
# Create masks with random shape
|
||||
# ###########################################################################
|
||||
|
||||
|
||||
def create_random_shape_with_random_motion(video_length,
|
||||
imageHeight=240,
|
||||
imageWidth=432):
|
||||
# get a random shape
|
||||
height = random.randint(imageHeight // 3, imageHeight - 1)
|
||||
width = random.randint(imageWidth // 3, imageWidth - 1)
|
||||
edge_num = random.randint(6, 8)
|
||||
ratio = random.randint(6, 8) / 10
|
||||
|
||||
region = get_random_shape(edge_num=edge_num,
|
||||
ratio=ratio,
|
||||
height=height,
|
||||
width=width)
|
||||
region_width, region_height = region.size
|
||||
# get random position
|
||||
x, y = random.randint(0, imageHeight - region_height), random.randint(
|
||||
0, imageWidth - region_width)
|
||||
velocity = get_random_velocity(max_speed=3)
|
||||
m = Image.fromarray(np.zeros((imageHeight, imageWidth)).astype(np.uint8))
|
||||
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
|
||||
masks = [m.convert('L')]
|
||||
# return fixed masks
|
||||
if random.uniform(0, 1) > 0.5:
|
||||
return masks * video_length
|
||||
# return moving masks
|
||||
for _ in range(video_length - 1):
|
||||
x, y, velocity = random_move_control_points(x,
|
||||
y,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
velocity,
|
||||
region.size,
|
||||
maxLineAcceleration=(3,
|
||||
0.5),
|
||||
maxInitSpeed=3)
|
||||
m = Image.fromarray(
|
||||
np.zeros((imageHeight, imageWidth)).astype(np.uint8))
|
||||
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
|
||||
masks.append(m.convert('L'))
|
||||
return masks
|
||||
|
||||
|
||||
def create_random_shape_with_random_motion_zoom_rotation(video_length, zoomin=0.9, zoomout=1.1, rotmin=1, rotmax=10, imageHeight=240, imageWidth=432):
|
||||
# get a random shape
|
||||
assert zoomin < 1, "Zoom-in parameter must be smaller than 1"
|
||||
assert zoomout > 1, "Zoom-out parameter must be larger than 1"
|
||||
assert rotmin < rotmax, "Minimum value of rotation must be smaller than maximun value !"
|
||||
height = random.randint(imageHeight//3, imageHeight-1)
|
||||
width = random.randint(imageWidth//3, imageWidth-1)
|
||||
edge_num = random.randint(6, 8)
|
||||
ratio = random.randint(6, 8)/10
|
||||
region = get_random_shape(
|
||||
edge_num=edge_num, ratio=ratio, height=height, width=width)
|
||||
region_width, region_height = region.size
|
||||
# get random position
|
||||
x, y = random.randint(
|
||||
0, imageHeight-region_height), random.randint(0, imageWidth-region_width)
|
||||
velocity = get_random_velocity(max_speed=3)
|
||||
m = Image.fromarray(np.zeros((imageHeight, imageWidth)).astype(np.uint8))
|
||||
m.paste(region, (y, x, y+region.size[0], x+region.size[1]))
|
||||
masks = [m.convert('L')]
|
||||
# return fixed masks
|
||||
if random.uniform(0, 1) > 0.5:
|
||||
return masks*video_length # -> directly copy all the base masks
|
||||
# return moving masks
|
||||
for _ in range(video_length-1):
|
||||
x, y, velocity = random_move_control_points(
|
||||
x, y, imageHeight, imageWidth, velocity, region.size, maxLineAcceleration=(3, 0.5), maxInitSpeed=3)
|
||||
m = Image.fromarray(
|
||||
np.zeros((imageHeight, imageWidth)).astype(np.uint8))
|
||||
### add by kaidong, to simulate zoon-in, zoom-out and rotation
|
||||
extra_transform = random.uniform(0, 1)
|
||||
# zoom in and zoom out
|
||||
if extra_transform > 0.75:
|
||||
resize_coefficient = random.uniform(zoomin, zoomout)
|
||||
region = region.resize((math.ceil(region_width * resize_coefficient), math.ceil(region_height * resize_coefficient)), Image.NEAREST)
|
||||
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
|
||||
region_width, region_height = region.size
|
||||
# rotation
|
||||
elif extra_transform > 0.5:
|
||||
m.paste(region, (y, x, y + region.size[0], x + region.size[1]))
|
||||
m = m.rotate(random.randint(rotmin, rotmax))
|
||||
# region_width, region_height = region.size
|
||||
### end
|
||||
else:
|
||||
m.paste(region, (y, x, y+region.size[0], x+region.size[1]))
|
||||
masks.append(m.convert('L'))
|
||||
return masks
|
||||
|
||||
|
||||
def get_random_shape(edge_num=9, ratio=0.7, width=432, height=240):
|
||||
'''
|
||||
There is the initial point and 3 points per cubic bezier curve.
|
||||
Thus, the curve will only pass though n points, which will be the sharp edges.
|
||||
The other 2 modify the shape of the bezier curve.
|
||||
edge_num, Number of possibly sharp edges
|
||||
points_num, number of points in the Path
|
||||
ratio, (0, 1) magnitude of the perturbation from the unit circle,
|
||||
'''
|
||||
points_num = edge_num*3 + 1
|
||||
angles = np.linspace(0, 2*np.pi, points_num)
|
||||
codes = np.full(points_num, Path.CURVE4)
|
||||
codes[0] = Path.MOVETO
|
||||
# Using this instead of Path.CLOSEPOLY avoids an innecessary straight line
|
||||
verts = np.stack((np.cos(angles), np.sin(angles))).T * \
|
||||
(2*ratio*np.random.random(points_num)+1-ratio)[:, None]
|
||||
verts[-1, :] = verts[0, :]
|
||||
path = Path(verts, codes)
|
||||
# draw paths into images
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111)
|
||||
patch = patches.PathPatch(path, facecolor='black', lw=2)
|
||||
ax.add_patch(patch)
|
||||
ax.set_xlim(np.min(verts)*1.1, np.max(verts)*1.1)
|
||||
ax.set_ylim(np.min(verts)*1.1, np.max(verts)*1.1)
|
||||
ax.axis('off') # removes the axis to leave only the shape
|
||||
fig.canvas.draw()
|
||||
# convert plt images into numpy images
|
||||
data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
|
||||
data = data.reshape((fig.canvas.get_width_height()[::-1] + (3,)))
|
||||
plt.close(fig)
|
||||
# postprocess
|
||||
data = cv2.resize(data, (width, height))[:, :, 0]
|
||||
data = (1 - np.array(data > 0).astype(np.uint8))*255
|
||||
corrdinates = np.where(data > 0)
|
||||
xmin, xmax, ymin, ymax = np.min(corrdinates[0]), np.max(
|
||||
corrdinates[0]), np.min(corrdinates[1]), np.max(corrdinates[1])
|
||||
region = Image.fromarray(data).crop((ymin, xmin, ymax, xmax))
|
||||
return region
|
||||
|
||||
|
||||
def random_accelerate(velocity, maxAcceleration, dist='uniform'):
|
||||
speed, angle = velocity
|
||||
d_speed, d_angle = maxAcceleration
|
||||
if dist == 'uniform':
|
||||
speed += np.random.uniform(-d_speed, d_speed)
|
||||
angle += np.random.uniform(-d_angle, d_angle)
|
||||
elif dist == 'guassian':
|
||||
speed += np.random.normal(0, d_speed / 2)
|
||||
angle += np.random.normal(0, d_angle / 2)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Distribution type {dist} is not supported.')
|
||||
return (speed, angle)
|
||||
|
||||
|
||||
def get_random_velocity(max_speed=3, dist='uniform'):
|
||||
if dist == 'uniform':
|
||||
speed = np.random.uniform(max_speed)
|
||||
elif dist == 'guassian':
|
||||
speed = np.abs(np.random.normal(0, max_speed / 2))
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'Distribution type {dist} is not supported.')
|
||||
angle = np.random.uniform(0, 2 * np.pi)
|
||||
return (speed, angle)
|
||||
|
||||
|
||||
def random_move_control_points(X,
|
||||
Y,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
lineVelocity,
|
||||
region_size,
|
||||
maxLineAcceleration=(3, 0.5),
|
||||
maxInitSpeed=3):
|
||||
region_width, region_height = region_size
|
||||
speed, angle = lineVelocity
|
||||
X += int(speed * np.cos(angle))
|
||||
Y += int(speed * np.sin(angle))
|
||||
lineVelocity = random_accelerate(lineVelocity,
|
||||
maxLineAcceleration,
|
||||
dist='guassian')
|
||||
if ((X > imageHeight - region_height) or (X < 0)
|
||||
or (Y > imageWidth - region_width) or (Y < 0)):
|
||||
lineVelocity = get_random_velocity(maxInitSpeed, dist='guassian')
|
||||
new_X = np.clip(X, 0, imageHeight - region_height)
|
||||
new_Y = np.clip(Y, 0, imageWidth - region_width)
|
||||
return new_X, new_Y, lineVelocity
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
trials = 10
|
||||
for _ in range(trials):
|
||||
video_length = 10
|
||||
# The returned masks are either stationary (50%) or moving (50%)
|
||||
masks = create_random_shape_with_random_motion(video_length,
|
||||
imageHeight=240,
|
||||
imageWidth=432)
|
||||
|
||||
for m in masks:
|
||||
cv2.imshow('mask', np.array(m))
|
||||
cv2.waitKey(500)
|
||||
@@ -0,0 +1 @@
|
||||
{"bear": 82, "blackswan": 50, "bmx-bumps": 90, "bmx-trees": 80, "boat": 75, "breakdance": 84, "breakdance-flare": 71, "bus": 80, "camel": 90, "car-roundabout": 75, "car-shadow": 40, "car-turn": 80, "cows": 104, "dance-jump": 60, "dance-twirl": 90, "dog": 60, "dog-agility": 25, "drift-chicane": 52, "drift-straight": 50, "drift-turn": 64, "elephant": 80, "flamingo": 80, "goat": 90, "hike": 80, "hockey": 75, "horsejump-high": 50, "horsejump-low": 60, "kite-surf": 50, "kite-walk": 80, "libby": 49, "lucia": 70, "mallard-fly": 70, "mallard-water": 80, "motocross-bumps": 60, "motocross-jump": 40, "motorbike": 43, "paragliding": 70, "paragliding-launch": 80, "parkour": 100, "rhino": 90, "rollerblade": 35, "scooter-black": 43, "scooter-gray": 75, "soapbox": 99, "soccerball": 48, "stroller": 91, "surf": 55, "swing": 60, "tennis": 70, "train": 80}
|
||||
@@ -0,0 +1 @@
|
||||
{"baseball": 90, "basketball-game": 77, "bears-ball": 78, "bmx-rider": 85, "butterfly": 80, "car-competition": 66, "cat": 52, "chairlift": 99, "circus": 73, "city-ride": 70, "crafting": 45, "curling": 76, "dog-competition": 85, "dolphins-show": 74, "dribbling": 49, "drone-flying": 70, "ducks": 75, "elephant-hyenas": 55, "giraffes": 88, "gym-ball": 69, "helicopter-landing": 77, "horse-race": 80, "horses-kids": 78, "hurdles-race": 55, "ice-hockey": 52, "jet-ski": 83, "juggling-selfie": 78, "kayak-race": 63, "kids-robot": 75, "landing": 35, "luggage": 83, "mantaray": 73, "marbles": 70, "mascot": 78, "mermaid": 78, "monster-trucks": 99, "motorbike-indoors": 79, "motorbike-race": 88, "music-band": 87, "obstacles": 81, "obstacles-race": 48, "peacock": 75, "plane-exhibition": 73, "puppet": 100, "robot-battle": 85, "robotic-arm": 82, "rodeo": 85, "sea-turtle": 90, "skydiving-jumping": 75, "snowboard-race": 75, "snowboard-sand": 55, "surfer": 80, "swimmer": 86, "table-tennis": 70, "tram": 84, "trucks-race": 78, "twist-dance": 83, "volleyball-beach": 73, "water-slide": 88, "weightlifting": 90}
|
||||
@@ -0,0 +1 @@
|
||||
{"0070461469": 91, "00bd64cb00": 180, "00fef116ee": 96, "012257ffcf": 180, "01475d1fe7": 180, "0163b18674": 96, "017fa2adaa": 180, "0232ba85ed": 180, "02b1a46f42": 180, "02caec8ac0": 91, "047436c72c": 96, "0481e165b4": 150, "04f98557e7": 144, "05e73c3ecb": 96, "08f95ce1ff": 144, "0b6db1c6fd": 96, "0bd8c18197": 180, "0c6d13ee2c": 91, "0c7ba00455": 96, "0cba3e52eb": 91, "0d16524447": 150, "0d4827437d": 150, "0d62fa582a": 180, "0e1f91c0d7": 91, "0ef454b3f0": 91, "10e18fcf0c": 96, "11105e147e": 91, "11444b16da": 91, "11a4df37a4": 180, "11b3298d6a": 96, "13006c4c7e": 96, "1345523ba1": 180, "144a16eb12": 180, "15a6536e74": 180, "1616507c9e": 180, "1655f4782a": 92, "16608ccef6": 96, "16bc05b66c": 150, "16f1e1779b": 96, "17caf00e26": 96, "18f1e2f716": 91, "191a0bfcdf": 180, "19d4acf831": 91, "1a1dc21969": 96, "1a72d9fcea": 150, "1a92c81edd": 180, "1b2c2022a3": 96, "1d1601d079": 180, "1db7b25d1c": 180, "1dee5b7b5a": 150, "1e0c2e54f2": 96, "1e458b1539": 92, "1e6ac08c86": 91, "1e790eae99": 56, "1ed0c6ca5b": 96, "1edbdb6d18": 180, "1f2015e056": 96, "215ac56b15": 180, "2233485b49": 96, "224d171af6": 180, "237c6ebaf4": 91, "2462c51412": 96, "24bf968338": 180, "250d5953a0": 150, "25bcf222fb": 180, "25ea8feecf": 150, "25fc493839": 92, "262f69837e": 180, "264ca20298": 180, "26d8d48248": 51, "270f84c5e5": 91, "27889bc0fe": 180, "29b87846e7": 96, "29d2e79171": 180, "2a44411a3d": 180, "2b426fd330": 180, "2c4c4e2d5b": 180, "2c4c718eda": 180, "2c962c1bbe": 180, "2cc841341c": 92, "2cf6c4d17e": 91, "2d7ef0be04": 180, "2e5e52c6c8": 150, "2ef6fce8c6": 144, "3014e769bf": 180, "30d5f163b6": 180, "318df73d6a": 90, "31fbb9df3c": 96, "3255fcad2f": 180, "3303eea8e4": 91, "3447c30052": 150, "362722660c": 180, "37e0b4642b": 91, "383e51ed93": 180, "386b050bd0": 41, "3876ba3136": 180, "388ec2934c": 180, "38b45d9c6b": 96, "396680839c": 150, "39ffa3a4a4": 180, "3b0291b2be": 150, "3b333693f4": 180, "3bde1da2cf": 96, "3c5f4e6672": 91, "3c80682cc6": 92, "3ce634a1c1": 180, "3d6a761295": 96, "3da878c317": 91, "3db571b7ee": 96, "3e2336812c": 180, "3f16b04d6d": 96, "3fbbc75c5e": 180, "4015a1e1cc": 87, "406cd7bd48": 91, "407b87ba26": 91, "40a5628dcc": 91, "41af239f5e": 180, "42c671b285": 180, "42de37f462": 180, "4381c60a2f": 180, "4445dc0af5": 180, "44a3419d24": 180, "4566034eaf": 51, "45877fd086": 180, "4595935b88": 91, "4923010cfe": 96, "49b6d81ee8": 180, "4a39c34139": 180, "4a5a9fde01": 144, "4a90394892": 180, "4af10534e4": 180, "4af307f5bc": 180, "4be0ac97df": 91, "4be9025726": 91, "4c18a7bfab": 91, "4c269afea9": 91, "4c3db058db": 179, "4e1ef26a1e": 96, "50f4c0195b": 150, "50f89963c0": 96, "5105c5e4b8": 180, "51d60e4f93": 46, "51ee638399": 96, "522ea1a892": 180, "528e9f30e7": 91, "532efb206a": 180, "544b1486ac": 91, "5592eb680c": 180, "562fadda3a": 91, "568b30cf93": 150, "575f0e2d8e": 91, "5767fe466c": 150, "581c78d558": 180, "5a0ddcf128": 96, "5adf056317": 144, "5b33c701ce": 180, "5b8f636b33": 150, "5b9d26b1d7": 180, "5c24813a0b": 180, "5d0b35f30f": 46, "5e130392e1": 96, "5e41efe5bc": 180, "5e75de78ae": 91, "5fc34880f7": 180, "60912d6bab": 96, "612c96383d": 180, "61e5fd2205": 144, "620e350d23": 180, "62c27fcaaf": 180, "637c22d967": 91, "63eaebe4a2": 96, "63fd6b311e": 180, "64099f32ab": 180, "65643c4b34": 96, "660a88feb5": 180, "664b8d0c9f": 150, "665a7947b0": 180, "66affc2e86": 180, "673b1c03c9": 96, "67780f49c2": 91, "679a24b7bd": 180, "680d35b75b": 144, "68364a69ef": 180, "683bfaf498": 180, "68e883ff28": 180, "691f63f681": 180, "69f2d3146c": 96, "6c5c018237": 91, "6caa33f43a": 96, "6d2c7cc107": 180, "6d55effbbe": 144, "6d6b09b420": 51, "6d715acc3e": 180, "6e89b7359d": 96, "6e9428d555": 150, "6e9feafa2b": 91, "6eced45fee": 180, "6ef0b3282c": 96, "6f9019f0ea": 91, "6fe0ee9b7c": 180, "6ff74d4995": 180, "712b6ec68e": 96, "71680a627f": 96, "716aad4b56": 180, "721c2cda07": 180, "72218d52ac": 96, "7286b8aac9": 91, "728ba7998d": 91, "73b2b9af5f": 96, "7452941f4f": 180, "759d8249dd": 91, "75a55907dc": 150, "75f3a2a19e": 150, "77e7e4b1a1": 144, "7898e6542c": 180, "78e639c2c4": 91, "79091168f8": 180, "7ad5af3fe6": 180, "7b1a7dec16": 150, "7b36c4c3db": 180, "7b455d07cc": 150, "7bce4cfa48": 180, "7c064444d0": 144, "7c8014406a": 91, "7cb70182e5": 96, "7d04e540f5": 91, "7d5df020bf": 96, "7dfda4322c": 96, "7e6a27cc7c": 96, "7e9e344bf4": 180, "7eb9424a53": 180, "7ec8ea61f4": 91, "7fd2806fb0": 180, "8006501830": 150, "8014aeb412": 180, "80d1d22999": 180, "812f31be15": 144, "81312af68f": 92, "82843a1676": 150, "835aea9584": 36, "8366c67e9b": 180, "8467aa6c5c": 180, "8470ee5f48": 180, "8473ae2c60": 180, "8519765a65": 150, "851f73e4fc": 96, "85621c2c81": 150, "85b045995c": 180, "860c0a7cf8": 92, "861bd4b31e": 180, "8639adb930": 180, "8683e4d414": 150, "8687e892ff": 180, "86c5907811": 180, "870c197c8b": 180, "87de455fb7": 180, "87e1975888": 96, "87f5d4903c": 96, "883ede763d": 150, "88b84fe107": 91, "88ee198ce0": 91, "89d148a39f": 96, "89f3d789c5": 180, "8a22bb6c32": 180, "8a76048654": 180, "8a99d63296": 97, "8b0697f61a": 96, "8b722babfb": 180, "8ba5691030": 180, "8bLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"003234408d": 180, "0043f083b5": 96, "0044fa5fba": 87, "005a527edd": 144, "0065b171f9": 180, "00917dcfc4": 96, "00a23ccf53": 180, "00ad5016a4": 91, "01082ae388": 150, "011ac0a06f": 180, "013099c098": 91, "0155498c85": 180, "01694ad9c8": 91, "017ac35701": 180, "01b80e8e1a": 61, "01baa5a4e1": 150, "01c3111683": 180, "01c4cb5ffe": 180, "01c76f0a82": 96, "01c783268c": 180, "01e64dd36a": 91, "01ed275c6e": 96, "01ff60d1fa": 180, "020cd28cd2": 150, "02264db755": 180, "0248626d9a": 91, "02668dbffa": 150, "0274193026": 144, "02d28375aa": 180, "02f3a5c4df": 46, "031ccc99b1": 91, "0321b18c10": 92, "0348a45bca": 180, "0355e92655": 92, "0358b938c1": 91, "0368107cf1": 96, "0379ddf557": 180, "038b2cc71d": 91, "038c15a5dd": 178, "03a06cc98a": 96, "03a63e187f": 180, "03c95b4dae": 92, "03e2b57b0e": 150, "04194e1248": 180, "04259896e2": 180, "0444918a5f": 96, "04460a7a52": 180, "04474174a4": 180, "0450095513": 150, "045f00aed2": 180, "04667fabaa": 180, "04735c5030": 91, "04990d1915": 92, "04d62d9d98": 96, "04f21da964": 180, "04fbad476e": 180, "04fe256562": 96, "0503bf89c9": 150, "0536c9eed0": 92, "054acb238f": 180, "05579ca250": 150, "056c200404": 96, "05774f3a2c": 180, "058a7592c8": 96, "05a0a513df": 96, "05a569d8aa": 91, "05aa652648": 150, "05d7715782": 96, "05e0b0f28f": 150, "05fdbbdd7a": 66, "05ffcfed85": 180, "0630391881": 150, "06840b2bbe": 91, "068f7dce6f": 180, "0693719753": 150, "06ce2b51fb": 91, "06e224798e": 180, "06ee361788": 91, "06fbb3fa2c": 90, "0700264286": 96, "070c918ca7": 180, "07129e14a4": 180, "07177017e9": 86, "07238ffc58": 180, "07353b2a89": 150, "0738493cbf": 87, "075926c651": 87, "075c701292": 180, "0762ea9a30": 96, "07652ee4af": 150, "076f206928": 96, "077d32af19": 96, "079049275c": 144, "07913cdda7": 92, "07a11a35e8": 180, "07ac33b6df": 150, "07b6e8fda8": 46, "07c62c3d11": 180, "07cc1c7d74": 180, "080196ef01": 180, "081207976e": 96, "081ae4fa44": 150, "081d8250cb": 96, "082900c5d4": 96, "0860df21e2": 180, "0866d4c5e3": 91, "0891ac2eb6": 81, "08931bc458": 180, "08aa2705d5": 180, "08c8450db7": 96, "08d50b926c": 180, "08e1e4de15": 180, "08e48c1a48": 92, "08f561c65e": 180, "08feb87790": 96, "09049f6fe3": 150, "092e4ff450": 180, "09338adea8": 180, "093c335ccc": 144, "0970d28339": 180, "0974a213dc": 96, "097b471ed8": 96, "0990941758": 180, "09a348f4fa": 150, "09a6841288": 96, "09c5bad17b": 96, "09c9ce80c7": 180, "09ff54fef4": 150, "0a23765d15": 91, "0a275e7f12": 96, "0a2f2bd294": 96, "0a7a2514aa": 96, "0a7b27fde9": 180, "0a8c467cc3": 180, "0ac8c560ae": 96, "0b1627e896": 96, "0b285c47f6": 144, "0b34ec1d55": 180, "0b5b5e8e5a": 96, "0b68535614": 180, "0b6f9105fc": 180, "0b7dbfa3cb": 91, "0b9cea51ca": 180, "0b9d012be8": 180, "0bcfc4177d": 96, "0bd37b23c1": 96, "0bd864064c": 158, "0c11c6bf7b": 180, "0c26bc77ac": 180, "0c3a04798c": 96, "0c44a9d545": 180, "0c817cc390": 180, "0ca839ee9a": 180, "0cd7ac0ac0": 150, "0ce06e0121": 180, "0cfe974a89": 180, "0d2fcc0dcd": 96, "0d3aad05d2": 144, "0d40b015f4": 180, "0d97fba242": 91, "0d9cc80d7e": 51, "0dab85b6d3": 144, "0db5c427a5": 96, "0dbaf284f1": 97, "0de4923598": 97, "0df28a9101": 150, "0e04f636c4": 150, "0e05f0e232": 180, "0e0930474b": 91, "0e27472bea": 180, "0e30020549": 144, "0e621feb6c": 180, "0e803c7d73": 91, "0e9ebe4e3c": 92, "0e9f2785ec": 96, "0ea68d418b": 96, "0eb403a222": 96, "0ee92053d6": 97, "0eefca067f": 150, "0f17fa6fcb": 180, "0f1ac8e9a3": 180, "0f202e9852": 91, "0f2ab8b1ff": 180, "0f51a78756": 150, "0f5fbe16b0": 180, "0f6072077b": 91, "0f6b69b2f4": 180, "0f6c2163de": 144, "0f74ec5599": 180, "0f9683715b": 96, "0fa7b59356": 180, "0fb173695b": 96, "0fc958cde2": 150, "0fe7b1a621": 180, "0ffcdb491c": 96, "101caff7d4": 96, "1022fe8417": 96, "1032e80b37": 96, "103f501680": 180, "104e64565f": 96, "104f1ab997": 91, "106242403f": 96, "10b31f5431": 180, "10eced835e": 91, "110d26fa3a": 150, "1122c1d16a": 180, "1145b49a5f": 180, "11485838c2": 96, "114e7676ec": 180, "1157472b95": 180, "115ee1072c": 91, "1171141012": 150, "117757b4b8": 180, "1178932d2f": 180, "117cc76bda": 180, "1180cbf814": 180, "1187bbd0e3": 96, "1197e44b26": 180, "119cf20728": 180, "119dd54871": 180, "11a0c3b724": 91, "11a6ba8c94": 180, "11c722a456": 180, "11cbcb0b4d": 96, "11ccf5e99d": 96, "11ce6f452e": 91, "11e53de6f2": 46, "11feabe596": 150, "120cb9514d": 180, "12156b25b3": 180, "122896672d": 180, "1232b2f1d4": 36, "1233ac8596": 97, "1239c87234": 180, "1250423f7c": 96, "1257a1bc67": 180, "125d1b19dd": 180, "126d203967": 180, "1295e19071": 96, "12ad198c54": 144, "12bddb2bcb": 150, "12ec9b93ee": 180, "12eebedc35": 91, "132852e094": 180, "1329409f2a": 180, "13325cfa14": 96, "1336440745": 180, "134d06dbf9": 97, "135625b53d": 144, "13870016f9": 92, "13960b3c84": 96, "13adaad9d9": 180, "13ae097e20": 180, "13e3070469": 96, "13f6a8c20d": 144, "1416925cf2": 92, "142d2621f5": 91, "145d5d7c03": 180, "145fdc3ac5": 180, "1471274fa7": 76, "14a6b5a139": 180, "14c21cea0d": 180, "14dae0dc93": 96, "14f9bd22b5": 180, "14fd28ae99": 180, "15097d5d4e": 144, "150ea711f2": 180, "1514e3563f": 180, "152aaa3a9e": 180, "1Line truncated
|
||||
@@ -0,0 +1,476 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import cv2
|
||||
import argparse
|
||||
import imageio
|
||||
import numpy as np
|
||||
import scipy.ndimage
|
||||
from PIL import Image
|
||||
from tqdm import tqdm
|
||||
|
||||
import torch
|
||||
import torchvision
|
||||
|
||||
from model.modules.flow_comp_raft import RAFT_bi
|
||||
from model.recurrent_flow_completion import RecurrentFlowCompleteNet
|
||||
from model.propainter import InpaintGenerator
|
||||
from utils.download_util import load_file_from_url
|
||||
from core.utils import to_tensors
|
||||
from model.misc import get_device
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
pretrain_model_url = 'https://github.com/sczhou/ProPainter/releases/download/v0.1.0/'
|
||||
|
||||
def imwrite(img, file_path, params=None, auto_mkdir=True):
|
||||
if auto_mkdir:
|
||||
dir_name = os.path.abspath(os.path.dirname(file_path))
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
return cv2.imwrite(file_path, img, params)
|
||||
|
||||
|
||||
# resize frames
|
||||
def resize_frames(frames, size=None):
|
||||
if size is not None:
|
||||
out_size = size
|
||||
process_size = (out_size[0]-out_size[0]%8, out_size[1]-out_size[1]%8)
|
||||
frames = [f.resize(process_size) for f in frames]
|
||||
else:
|
||||
out_size = frames[0].size
|
||||
process_size = (out_size[0]-out_size[0]%8, out_size[1]-out_size[1]%8)
|
||||
if not out_size == process_size:
|
||||
frames = [f.resize(process_size) for f in frames]
|
||||
|
||||
return frames, process_size, out_size
|
||||
|
||||
|
||||
# read frames from video
|
||||
def read_frame_from_videos(frame_root):
|
||||
if frame_root.endswith(('mp4', 'mov', 'avi', 'MP4', 'MOV', 'AVI')): # input video path
|
||||
video_name = os.path.basename(frame_root)[:-4]
|
||||
vframes, aframes, info = torchvision.io.read_video(filename=frame_root, pts_unit='sec') # RGB
|
||||
frames = list(vframes.numpy())
|
||||
frames = [Image.fromarray(f) for f in frames]
|
||||
fps = info['video_fps']
|
||||
else:
|
||||
video_name = os.path.basename(frame_root)
|
||||
frames = []
|
||||
fr_lst = sorted(os.listdir(frame_root))
|
||||
for fr in fr_lst:
|
||||
frame = cv2.imread(os.path.join(frame_root, fr))
|
||||
frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
||||
frames.append(frame)
|
||||
fps = None
|
||||
size = frames[0].size
|
||||
|
||||
return frames, fps, size, video_name
|
||||
|
||||
|
||||
def binary_mask(mask, th=0.1):
|
||||
mask[mask>th] = 1
|
||||
mask[mask<=th] = 0
|
||||
return mask
|
||||
|
||||
|
||||
# read frame-wise masks
|
||||
def read_mask(mpath, length, size, flow_mask_dilates=8, mask_dilates=5):
|
||||
masks_img = []
|
||||
masks_dilated = []
|
||||
flow_masks = []
|
||||
|
||||
if mpath.endswith(('jpg', 'jpeg', 'png', 'JPG', 'JPEG', 'PNG')): # input single img path
|
||||
masks_img = [Image.open(mpath)]
|
||||
else:
|
||||
mnames = sorted(os.listdir(mpath))
|
||||
for mp in mnames:
|
||||
masks_img.append(Image.open(os.path.join(mpath, mp)))
|
||||
|
||||
for mask_img in masks_img:
|
||||
if size is not None:
|
||||
mask_img = mask_img.resize(size, Image.NEAREST)
|
||||
mask_img = np.array(mask_img.convert('L'))
|
||||
|
||||
# Dilate 8 pixel so that all known pixel is trustworthy
|
||||
if flow_mask_dilates > 0:
|
||||
flow_mask_img = scipy.ndimage.binary_dilation(mask_img, iterations=flow_mask_dilates).astype(np.uint8)
|
||||
else:
|
||||
flow_mask_img = binary_mask(mask_img).astype(np.uint8)
|
||||
# Close the small holes inside the foreground objects
|
||||
# flow_mask_img = cv2.morphologyEx(flow_mask_img, cv2.MORPH_CLOSE, np.ones((21, 21),np.uint8)).astype(bool)
|
||||
# flow_mask_img = scipy.ndimage.binary_fill_holes(flow_mask_img).astype(np.uint8)
|
||||
flow_masks.append(Image.fromarray(flow_mask_img * 255))
|
||||
|
||||
if mask_dilates > 0:
|
||||
mask_img = scipy.ndimage.binary_dilation(mask_img, iterations=mask_dilates).astype(np.uint8)
|
||||
else:
|
||||
mask_img = binary_mask(mask_img).astype(np.uint8)
|
||||
masks_dilated.append(Image.fromarray(mask_img * 255))
|
||||
|
||||
if len(masks_img) == 1:
|
||||
flow_masks = flow_masks * length
|
||||
masks_dilated = masks_dilated * length
|
||||
|
||||
return flow_masks, masks_dilated
|
||||
|
||||
|
||||
def extrapolation(video_ori, scale):
|
||||
"""Prepares the data for video outpainting.
|
||||
"""
|
||||
nFrame = len(video_ori)
|
||||
imgW, imgH = video_ori[0].size
|
||||
|
||||
# Defines new FOV.
|
||||
imgH_extr = int(scale[0] * imgH)
|
||||
imgW_extr = int(scale[1] * imgW)
|
||||
imgH_extr = imgH_extr - imgH_extr % 8
|
||||
imgW_extr = imgW_extr - imgW_extr % 8
|
||||
H_start = int((imgH_extr - imgH) / 2)
|
||||
W_start = int((imgW_extr - imgW) / 2)
|
||||
|
||||
# Extrapolates the FOV for video.
|
||||
frames = []
|
||||
for v in video_ori:
|
||||
frame = np.zeros(((imgH_extr, imgW_extr, 3)), dtype=np.uint8)
|
||||
frame[H_start: H_start + imgH, W_start: W_start + imgW, :] = v
|
||||
frames.append(Image.fromarray(frame))
|
||||
|
||||
# Generates the mask for missing region.
|
||||
masks_dilated = []
|
||||
flow_masks = []
|
||||
|
||||
dilate_h = 4 if H_start > 10 else 0
|
||||
dilate_w = 4 if W_start > 10 else 0
|
||||
mask = np.ones(((imgH_extr, imgW_extr)), dtype=np.uint8)
|
||||
|
||||
mask[H_start+dilate_h: H_start+imgH-dilate_h,
|
||||
W_start+dilate_w: W_start+imgW-dilate_w] = 0
|
||||
flow_masks.append(Image.fromarray(mask * 255))
|
||||
|
||||
mask[H_start: H_start+imgH, W_start: W_start+imgW] = 0
|
||||
masks_dilated.append(Image.fromarray(mask * 255))
|
||||
|
||||
flow_masks = flow_masks * nFrame
|
||||
masks_dilated = masks_dilated * nFrame
|
||||
|
||||
return frames, flow_masks, masks_dilated, (imgW_extr, imgH_extr)
|
||||
|
||||
|
||||
def get_ref_index(mid_neighbor_id, neighbor_ids, length, ref_stride=10, ref_num=-1):
|
||||
ref_index = []
|
||||
if ref_num == -1:
|
||||
for i in range(0, length, ref_stride):
|
||||
if i not in neighbor_ids:
|
||||
ref_index.append(i)
|
||||
else:
|
||||
start_idx = max(0, mid_neighbor_id - ref_stride * (ref_num // 2))
|
||||
end_idx = min(length, mid_neighbor_id + ref_stride * (ref_num // 2))
|
||||
for i in range(start_idx, end_idx, ref_stride):
|
||||
if i not in neighbor_ids:
|
||||
if len(ref_index) > ref_num:
|
||||
break
|
||||
ref_index.append(i)
|
||||
return ref_index
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
device = get_device()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'-i', '--video', type=str, default='inputs/object_removal/bmx-trees', help='Path of the input video or image folder.')
|
||||
parser.add_argument(
|
||||
'-m', '--mask', type=str, default='inputs/object_removal/bmx-trees_mask', help='Path of the mask(s) or mask folder.')
|
||||
parser.add_argument(
|
||||
'-o', '--output', type=str, default='results', help='Output folder. Default: results')
|
||||
parser.add_argument(
|
||||
"--resize_ratio", type=float, default=1.0, help='Resize scale for processing video.')
|
||||
parser.add_argument(
|
||||
'--height', type=int, default=-1, help='Height of the processing video.')
|
||||
parser.add_argument(
|
||||
'--width', type=int, default=-1, help='Width of the processing video.')
|
||||
parser.add_argument(
|
||||
'--mask_dilation', type=int, default=4, help='Mask dilation for video and flow masking.')
|
||||
parser.add_argument(
|
||||
"--ref_stride", type=int, default=10, help='Stride of global reference frames.')
|
||||
parser.add_argument(
|
||||
"--neighbor_length", type=int, default=10, help='Length of local neighboring frames.')
|
||||
parser.add_argument(
|
||||
"--subvideo_length", type=int, default=80, help='Length of sub-video for long video inference.')
|
||||
parser.add_argument(
|
||||
"--raft_iter", type=int, default=20, help='Iterations for RAFT inference.')
|
||||
parser.add_argument(
|
||||
'--mode', default='video_inpainting', choices=['video_inpainting', 'video_outpainting'], help="Modes: video_inpainting / video_outpainting")
|
||||
parser.add_argument(
|
||||
'--scale_h', type=float, default=1.0, help='Outpainting scale of height for video_outpainting mode.')
|
||||
parser.add_argument(
|
||||
'--scale_w', type=float, default=1.2, help='Outpainting scale of width for video_outpainting mode.')
|
||||
parser.add_argument(
|
||||
'--save_fps', type=int, default=24, help='Frame per second. Default: 24')
|
||||
parser.add_argument(
|
||||
'--save_frames', action='store_true', help='Save output frames. Default: False')
|
||||
parser.add_argument(
|
||||
'--fp16', action='store_true', help='Use fp16 (half precision) during inference. Default: fp32 (single precision).')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Use fp16 precision during inference to reduce running memory cost
|
||||
use_half = True if args.fp16 else False
|
||||
if device == torch.device('cpu'):
|
||||
use_half = False
|
||||
|
||||
frames, fps, size, video_name = read_frame_from_videos(args.video)
|
||||
if not args.width == -1 and not args.height == -1:
|
||||
size = (args.width, args.height)
|
||||
if not args.resize_ratio == 1.0:
|
||||
size = (int(args.resize_ratio * size[0]), int(args.resize_ratio * size[1]))
|
||||
|
||||
frames, size, out_size = resize_frames(frames, size)
|
||||
|
||||
fps = args.save_fps if fps is None else fps
|
||||
save_root = os.path.join(args.output, video_name)
|
||||
if not os.path.exists(save_root):
|
||||
os.makedirs(save_root, exist_ok=True)
|
||||
|
||||
if args.mode == 'video_inpainting':
|
||||
frames_len = len(frames)
|
||||
flow_masks, masks_dilated = read_mask(args.mask, frames_len, size,
|
||||
flow_mask_dilates=args.mask_dilation,
|
||||
mask_dilates=args.mask_dilation)
|
||||
w, h = size
|
||||
elif args.mode == 'video_outpainting':
|
||||
assert args.scale_h is not None and args.scale_w is not None, 'Please provide a outpainting scale (s_h, s_w).'
|
||||
frames, flow_masks, masks_dilated, size = extrapolation(frames, (args.scale_h, args.scale_w))
|
||||
w, h = size
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
# for saving the masked frames or video
|
||||
masked_frame_for_save = []
|
||||
for i in range(len(frames)):
|
||||
mask_ = np.expand_dims(np.array(masks_dilated[i]),2).repeat(3, axis=2)/255.
|
||||
img = np.array(frames[i])
|
||||
green = np.zeros([h, w, 3])
|
||||
green[:,:,1] = 255
|
||||
alpha = 0.6
|
||||
# alpha = 1.0
|
||||
fuse_img = (1-alpha)*img + alpha*green
|
||||
fuse_img = mask_ * fuse_img + (1-mask_)*img
|
||||
masked_frame_for_save.append(fuse_img.astype(np.uint8))
|
||||
|
||||
frames_inp = [np.array(f).astype(np.uint8) for f in frames]
|
||||
frames = to_tensors()(frames).unsqueeze(0) * 2 - 1
|
||||
flow_masks = to_tensors()(flow_masks).unsqueeze(0)
|
||||
masks_dilated = to_tensors()(masks_dilated).unsqueeze(0)
|
||||
frames, flow_masks, masks_dilated = frames.to(device), flow_masks.to(device), masks_dilated.to(device)
|
||||
|
||||
|
||||
##############################################
|
||||
# set up RAFT and flow competition model
|
||||
##############################################
|
||||
ckpt_path = load_file_from_url(url=os.path.join(pretrain_model_url, 'raft-things.pth'),
|
||||
model_dir='weights', progress=True, file_name=None)
|
||||
fix_raft = RAFT_bi(ckpt_path, device)
|
||||
|
||||
ckpt_path = load_file_from_url(url=os.path.join(pretrain_model_url, 'recurrent_flow_completion.pth'),
|
||||
model_dir='weights', progress=True, file_name=None)
|
||||
fix_flow_complete = RecurrentFlowCompleteNet(ckpt_path)
|
||||
for p in fix_flow_complete.parameters():
|
||||
p.requires_grad = False
|
||||
fix_flow_complete.to(device)
|
||||
fix_flow_complete.eval()
|
||||
|
||||
|
||||
##############################################
|
||||
# set up ProPainter model
|
||||
##############################################
|
||||
ckpt_path = load_file_from_url(url=os.path.join(pretrain_model_url, 'ProPainter.pth'),
|
||||
model_dir='weights', progress=True, file_name=None)
|
||||
model = InpaintGenerator(model_path=ckpt_path).to(device)
|
||||
model.eval()
|
||||
|
||||
|
||||
##############################################
|
||||
# ProPainter inference
|
||||
##############################################
|
||||
video_length = frames.size(1)
|
||||
print(f'\nProcessing: {video_name} [{video_length} frames]...')
|
||||
with torch.no_grad():
|
||||
# ---- compute flow ----
|
||||
if frames.size(-1) <= 640:
|
||||
short_clip_len = 12
|
||||
elif frames.size(-1) <= 720:
|
||||
short_clip_len = 8
|
||||
elif frames.size(-1) <= 1280:
|
||||
short_clip_len = 4
|
||||
else:
|
||||
short_clip_len = 2
|
||||
|
||||
# use fp32 for RAFT
|
||||
if frames.size(1) > short_clip_len:
|
||||
gt_flows_f_list, gt_flows_b_list = [], []
|
||||
for f in range(0, video_length, short_clip_len):
|
||||
end_f = min(video_length, f + short_clip_len)
|
||||
if f == 0:
|
||||
flows_f, flows_b = fix_raft(frames[:,f:end_f], iters=args.raft_iter)
|
||||
else:
|
||||
flows_f, flows_b = fix_raft(frames[:,f-1:end_f], iters=args.raft_iter)
|
||||
|
||||
gt_flows_f_list.append(flows_f)
|
||||
gt_flows_b_list.append(flows_b)
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
gt_flows_f = torch.cat(gt_flows_f_list, dim=1)
|
||||
gt_flows_b = torch.cat(gt_flows_b_list, dim=1)
|
||||
gt_flows_bi = (gt_flows_f, gt_flows_b)
|
||||
else:
|
||||
gt_flows_bi = fix_raft(frames, iters=args.raft_iter)
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
if use_half:
|
||||
frames, flow_masks, masks_dilated = frames.half(), flow_masks.half(), masks_dilated.half()
|
||||
gt_flows_bi = (gt_flows_bi[0].half(), gt_flows_bi[1].half())
|
||||
fix_flow_complete = fix_flow_complete.half()
|
||||
model = model.half()
|
||||
|
||||
|
||||
# ---- complete flow ----
|
||||
flow_length = gt_flows_bi[0].size(1)
|
||||
if flow_length > args.subvideo_length:
|
||||
pred_flows_f, pred_flows_b = [], []
|
||||
pad_len = 5
|
||||
for f in range(0, flow_length, args.subvideo_length):
|
||||
s_f = max(0, f - pad_len)
|
||||
e_f = min(flow_length, f + args.subvideo_length + pad_len)
|
||||
pad_len_s = max(0, f) - s_f
|
||||
pad_len_e = e_f - min(flow_length, f + args.subvideo_length)
|
||||
pred_flows_bi_sub, _ = fix_flow_complete.forward_bidirect_flow(
|
||||
(gt_flows_bi[0][:, s_f:e_f], gt_flows_bi[1][:, s_f:e_f]),
|
||||
flow_masks[:, s_f:e_f+1])
|
||||
pred_flows_bi_sub = fix_flow_complete.combine_flow(
|
||||
(gt_flows_bi[0][:, s_f:e_f], gt_flows_bi[1][:, s_f:e_f]),
|
||||
pred_flows_bi_sub,
|
||||
flow_masks[:, s_f:e_f+1])
|
||||
|
||||
pred_flows_f.append(pred_flows_bi_sub[0][:, pad_len_s:e_f-s_f-pad_len_e])
|
||||
pred_flows_b.append(pred_flows_bi_sub[1][:, pad_len_s:e_f-s_f-pad_len_e])
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
pred_flows_f = torch.cat(pred_flows_f, dim=1)
|
||||
pred_flows_b = torch.cat(pred_flows_b, dim=1)
|
||||
pred_flows_bi = (pred_flows_f, pred_flows_b)
|
||||
else:
|
||||
pred_flows_bi, _ = fix_flow_complete.forward_bidirect_flow(gt_flows_bi, flow_masks)
|
||||
pred_flows_bi = fix_flow_complete.combine_flow(gt_flows_bi, pred_flows_bi, flow_masks)
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
# ---- image propagation ----
|
||||
masked_frames = frames * (1 - masks_dilated)
|
||||
subvideo_length_img_prop = min(100, args.subvideo_length) # ensure a minimum of 100 frames for image propagation
|
||||
if video_length > subvideo_length_img_prop:
|
||||
updated_frames, updated_masks = [], []
|
||||
pad_len = 10
|
||||
for f in range(0, video_length, subvideo_length_img_prop):
|
||||
s_f = max(0, f - pad_len)
|
||||
e_f = min(video_length, f + subvideo_length_img_prop + pad_len)
|
||||
pad_len_s = max(0, f) - s_f
|
||||
pad_len_e = e_f - min(video_length, f + subvideo_length_img_prop)
|
||||
|
||||
b, t, _, _, _ = masks_dilated[:, s_f:e_f].size()
|
||||
pred_flows_bi_sub = (pred_flows_bi[0][:, s_f:e_f-1], pred_flows_bi[1][:, s_f:e_f-1])
|
||||
prop_imgs_sub, updated_local_masks_sub = model.img_propagation(masked_frames[:, s_f:e_f],
|
||||
pred_flows_bi_sub,
|
||||
masks_dilated[:, s_f:e_f],
|
||||
'nearest')
|
||||
updated_frames_sub = frames[:, s_f:e_f] * (1 - masks_dilated[:, s_f:e_f]) + \
|
||||
prop_imgs_sub.view(b, t, 3, h, w) * masks_dilated[:, s_f:e_f]
|
||||
updated_masks_sub = updated_local_masks_sub.view(b, t, 1, h, w)
|
||||
|
||||
updated_frames.append(updated_frames_sub[:, pad_len_s:e_f-s_f-pad_len_e])
|
||||
updated_masks.append(updated_masks_sub[:, pad_len_s:e_f-s_f-pad_len_e])
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
updated_frames = torch.cat(updated_frames, dim=1)
|
||||
updated_masks = torch.cat(updated_masks, dim=1)
|
||||
else:
|
||||
b, t, _, _, _ = masks_dilated.size()
|
||||
prop_imgs, updated_local_masks = model.img_propagation(masked_frames, pred_flows_bi, masks_dilated, 'nearest')
|
||||
updated_frames = frames * (1 - masks_dilated) + prop_imgs.view(b, t, 3, h, w) * masks_dilated
|
||||
updated_masks = updated_local_masks.view(b, t, 1, h, w)
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
ori_frames = frames_inp
|
||||
comp_frames = [None] * video_length
|
||||
|
||||
neighbor_stride = args.neighbor_length // 2
|
||||
if video_length > args.subvideo_length:
|
||||
ref_num = args.subvideo_length // args.ref_stride
|
||||
else:
|
||||
ref_num = -1
|
||||
|
||||
# ---- feature propagation + transformer ----
|
||||
for f in tqdm(range(0, video_length, neighbor_stride)):
|
||||
neighbor_ids = [
|
||||
i for i in range(max(0, f - neighbor_stride),
|
||||
min(video_length, f + neighbor_stride + 1))
|
||||
]
|
||||
ref_ids = get_ref_index(f, neighbor_ids, video_length, args.ref_stride, ref_num)
|
||||
selected_imgs = updated_frames[:, neighbor_ids + ref_ids, :, :, :]
|
||||
selected_masks = masks_dilated[:, neighbor_ids + ref_ids, :, :, :]
|
||||
selected_update_masks = updated_masks[:, neighbor_ids + ref_ids, :, :, :]
|
||||
selected_pred_flows_bi = (pred_flows_bi[0][:, neighbor_ids[:-1], :, :, :], pred_flows_bi[1][:, neighbor_ids[:-1], :, :, :])
|
||||
|
||||
with torch.no_grad():
|
||||
# 1.0 indicates mask
|
||||
l_t = len(neighbor_ids)
|
||||
|
||||
# pred_img = selected_imgs # results of image propagation
|
||||
pred_img = model(selected_imgs, selected_pred_flows_bi, selected_masks, selected_update_masks, l_t)
|
||||
|
||||
pred_img = pred_img.view(-1, 3, h, w)
|
||||
|
||||
pred_img = (pred_img + 1) / 2
|
||||
pred_img = pred_img.cpu().permute(0, 2, 3, 1).numpy() * 255
|
||||
binary_masks = masks_dilated[0, neighbor_ids, :, :, :].cpu().permute(
|
||||
0, 2, 3, 1).numpy().astype(np.uint8)
|
||||
for i in range(len(neighbor_ids)):
|
||||
idx = neighbor_ids[i]
|
||||
img = np.array(pred_img[i]).astype(np.uint8) * binary_masks[i] \
|
||||
+ ori_frames[idx] * (1 - binary_masks[i])
|
||||
if comp_frames[idx] is None:
|
||||
comp_frames[idx] = img
|
||||
else:
|
||||
comp_frames[idx] = comp_frames[idx].astype(np.float32) * 0.5 + img.astype(np.float32) * 0.5
|
||||
|
||||
comp_frames[idx] = comp_frames[idx].astype(np.uint8)
|
||||
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# save each frame
|
||||
if args.save_frames:
|
||||
for idx in range(video_length):
|
||||
f = comp_frames[idx]
|
||||
f = cv2.resize(f, out_size, interpolation = cv2.INTER_CUBIC)
|
||||
f = cv2.cvtColor(f, cv2.COLOR_BGR2RGB)
|
||||
img_save_root = os.path.join(save_root, 'frames', str(idx).zfill(4)+'.png')
|
||||
imwrite(f, img_save_root)
|
||||
|
||||
|
||||
# if args.mode == 'video_outpainting':
|
||||
# comp_frames = [i[10:-10,10:-10] for i in comp_frames]
|
||||
# masked_frame_for_save = [i[10:-10,10:-10] for i in masked_frame_for_save]
|
||||
|
||||
# save videos frame
|
||||
masked_frame_for_save = [cv2.resize(f, out_size) for f in masked_frame_for_save]
|
||||
comp_frames = [cv2.resize(f, out_size) for f in comp_frames]
|
||||
imageio.mimwrite(os.path.join(save_root, 'masked_in.mp4'), masked_frame_for_save, fps=fps, quality=7)
|
||||
imageio.mimwrite(os.path.join(save_root, 'inpaint_out.mp4'), comp_frames, fps=fps, quality=7)
|
||||
|
||||
print(f'\nAll results are saved in {save_root}')
|
||||
|
||||
torch.cuda.empty_cache()
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 59 KiB |