Spaces:
Paused
Paused
Upload 13 files
Browse files- basicsr/utils/__init__.py +29 -0
- basicsr/utils/dist_util.py +82 -0
- basicsr/utils/download_util.py +95 -0
- basicsr/utils/file_client.py +167 -0
- basicsr/utils/img_util.py +171 -0
- basicsr/utils/lmdb_util.py +196 -0
- basicsr/utils/logger.py +169 -0
- basicsr/utils/matlab_functions.py +347 -0
- basicsr/utils/misc.py +157 -0
- basicsr/utils/options.py +108 -0
- basicsr/utils/realesrgan_utils.py +302 -0
- basicsr/utils/registry.py +82 -0
- basicsr/utils/video_util.py +125 -0
basicsr/utils/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .file_client import FileClient
|
| 2 |
+
from .img_util import crop_border, imfrombytes, img2tensor, imwrite, tensor2img
|
| 3 |
+
from .logger import MessageLogger, get_env_info, get_root_logger, init_tb_logger, init_wandb_logger
|
| 4 |
+
from .misc import check_resume, get_time_str, make_exp_dirs, mkdir_and_rename, scandir, set_random_seed, sizeof_fmt
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
# file_client.py
|
| 8 |
+
'FileClient',
|
| 9 |
+
# img_util.py
|
| 10 |
+
'img2tensor',
|
| 11 |
+
'tensor2img',
|
| 12 |
+
'imfrombytes',
|
| 13 |
+
'imwrite',
|
| 14 |
+
'crop_border',
|
| 15 |
+
# logger.py
|
| 16 |
+
'MessageLogger',
|
| 17 |
+
'init_tb_logger',
|
| 18 |
+
'init_wandb_logger',
|
| 19 |
+
'get_root_logger',
|
| 20 |
+
'get_env_info',
|
| 21 |
+
# misc.py
|
| 22 |
+
'set_random_seed',
|
| 23 |
+
'get_time_str',
|
| 24 |
+
'mkdir_and_rename',
|
| 25 |
+
'make_exp_dirs',
|
| 26 |
+
'scandir',
|
| 27 |
+
'check_resume',
|
| 28 |
+
'sizeof_fmt'
|
| 29 |
+
]
|
basicsr/utils/dist_util.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified from https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/dist_utils.py # noqa: E501
|
| 2 |
+
import functools
|
| 3 |
+
import os
|
| 4 |
+
import subprocess
|
| 5 |
+
import torch
|
| 6 |
+
import torch.distributed as dist
|
| 7 |
+
import torch.multiprocessing as mp
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def init_dist(launcher, backend='nccl', **kwargs):
|
| 11 |
+
if mp.get_start_method(allow_none=True) is None:
|
| 12 |
+
mp.set_start_method('spawn')
|
| 13 |
+
if launcher == 'pytorch':
|
| 14 |
+
_init_dist_pytorch(backend, **kwargs)
|
| 15 |
+
elif launcher == 'slurm':
|
| 16 |
+
_init_dist_slurm(backend, **kwargs)
|
| 17 |
+
else:
|
| 18 |
+
raise ValueError(f'Invalid launcher type: {launcher}')
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _init_dist_pytorch(backend, **kwargs):
|
| 22 |
+
rank = int(os.environ['RANK'])
|
| 23 |
+
num_gpus = torch.cuda.device_count()
|
| 24 |
+
torch.cuda.set_device(rank % num_gpus)
|
| 25 |
+
dist.init_process_group(backend=backend, **kwargs)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _init_dist_slurm(backend, port=None):
|
| 29 |
+
"""Initialize slurm distributed training environment.
|
| 30 |
+
|
| 31 |
+
If argument ``port`` is not specified, then the master port will be system
|
| 32 |
+
environment variable ``MASTER_PORT``. If ``MASTER_PORT`` is not in system
|
| 33 |
+
environment variable, then a default port ``29500`` will be used.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
backend (str): Backend of torch.distributed.
|
| 37 |
+
port (int, optional): Master port. Defaults to None.
|
| 38 |
+
"""
|
| 39 |
+
proc_id = int(os.environ['SLURM_PROCID'])
|
| 40 |
+
ntasks = int(os.environ['SLURM_NTASKS'])
|
| 41 |
+
node_list = os.environ['SLURM_NODELIST']
|
| 42 |
+
num_gpus = torch.cuda.device_count()
|
| 43 |
+
torch.cuda.set_device(proc_id % num_gpus)
|
| 44 |
+
addr = subprocess.getoutput(f'scontrol show hostname {node_list} | head -n1')
|
| 45 |
+
# specify master port
|
| 46 |
+
if port is not None:
|
| 47 |
+
os.environ['MASTER_PORT'] = str(port)
|
| 48 |
+
elif 'MASTER_PORT' in os.environ:
|
| 49 |
+
pass # use MASTER_PORT in the environment variable
|
| 50 |
+
else:
|
| 51 |
+
# 29500 is torch.distributed default port
|
| 52 |
+
os.environ['MASTER_PORT'] = '29500'
|
| 53 |
+
os.environ['MASTER_ADDR'] = addr
|
| 54 |
+
os.environ['WORLD_SIZE'] = str(ntasks)
|
| 55 |
+
os.environ['LOCAL_RANK'] = str(proc_id % num_gpus)
|
| 56 |
+
os.environ['RANK'] = str(proc_id)
|
| 57 |
+
dist.init_process_group(backend=backend)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def get_dist_info():
|
| 61 |
+
if dist.is_available():
|
| 62 |
+
initialized = dist.is_initialized()
|
| 63 |
+
else:
|
| 64 |
+
initialized = False
|
| 65 |
+
if initialized:
|
| 66 |
+
rank = dist.get_rank()
|
| 67 |
+
world_size = dist.get_world_size()
|
| 68 |
+
else:
|
| 69 |
+
rank = 0
|
| 70 |
+
world_size = 1
|
| 71 |
+
return rank, world_size
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def master_only(func):
|
| 75 |
+
|
| 76 |
+
@functools.wraps(func)
|
| 77 |
+
def wrapper(*args, **kwargs):
|
| 78 |
+
rank, _ = get_dist_info()
|
| 79 |
+
if rank == 0:
|
| 80 |
+
return func(*args, **kwargs)
|
| 81 |
+
|
| 82 |
+
return wrapper
|
basicsr/utils/download_util.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import os
|
| 3 |
+
import requests
|
| 4 |
+
from torch.hub import download_url_to_file, get_dir
|
| 5 |
+
from tqdm import tqdm
|
| 6 |
+
from urllib.parse import urlparse
|
| 7 |
+
|
| 8 |
+
from .misc import sizeof_fmt
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def download_file_from_google_drive(file_id, save_path):
|
| 12 |
+
"""Download files from google drive.
|
| 13 |
+
Ref:
|
| 14 |
+
https://stackoverflow.com/questions/25010369/wget-curl-large-file-from-google-drive # noqa E501
|
| 15 |
+
Args:
|
| 16 |
+
file_id (str): File id.
|
| 17 |
+
save_path (str): Save path.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
session = requests.Session()
|
| 21 |
+
URL = 'https://docs.google.com/uc?export=download'
|
| 22 |
+
params = {'id': file_id}
|
| 23 |
+
|
| 24 |
+
response = session.get(URL, params=params, stream=True)
|
| 25 |
+
token = get_confirm_token(response)
|
| 26 |
+
if token:
|
| 27 |
+
params['confirm'] = token
|
| 28 |
+
response = session.get(URL, params=params, stream=True)
|
| 29 |
+
|
| 30 |
+
# get file size
|
| 31 |
+
response_file_size = session.get(URL, params=params, stream=True, headers={'Range': 'bytes=0-2'})
|
| 32 |
+
print(response_file_size)
|
| 33 |
+
if 'Content-Range' in response_file_size.headers:
|
| 34 |
+
file_size = int(response_file_size.headers['Content-Range'].split('/')[1])
|
| 35 |
+
else:
|
| 36 |
+
file_size = None
|
| 37 |
+
|
| 38 |
+
save_response_content(response, save_path, file_size)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def get_confirm_token(response):
|
| 42 |
+
for key, value in response.cookies.items():
|
| 43 |
+
if key.startswith('download_warning'):
|
| 44 |
+
return value
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def save_response_content(response, destination, file_size=None, chunk_size=32768):
|
| 49 |
+
if file_size is not None:
|
| 50 |
+
pbar = tqdm(total=math.ceil(file_size / chunk_size), unit='chunk')
|
| 51 |
+
|
| 52 |
+
readable_file_size = sizeof_fmt(file_size)
|
| 53 |
+
else:
|
| 54 |
+
pbar = None
|
| 55 |
+
|
| 56 |
+
with open(destination, 'wb') as f:
|
| 57 |
+
downloaded_size = 0
|
| 58 |
+
for chunk in response.iter_content(chunk_size):
|
| 59 |
+
downloaded_size += chunk_size
|
| 60 |
+
if pbar is not None:
|
| 61 |
+
pbar.update(1)
|
| 62 |
+
pbar.set_description(f'Download {sizeof_fmt(downloaded_size)} / {readable_file_size}')
|
| 63 |
+
if chunk: # filter out keep-alive new chunks
|
| 64 |
+
f.write(chunk)
|
| 65 |
+
if pbar is not None:
|
| 66 |
+
pbar.close()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def load_file_from_url(url, model_dir=None, progress=True, file_name=None):
|
| 70 |
+
"""Load file form http url, will download models if necessary.
|
| 71 |
+
Ref:https://github.com/1adrianb/face-alignment/blob/master/face_alignment/utils.py
|
| 72 |
+
Args:
|
| 73 |
+
url (str): URL to be downloaded.
|
| 74 |
+
model_dir (str): The path to save the downloaded model. Should be a full path. If None, use pytorch hub_dir.
|
| 75 |
+
Default: None.
|
| 76 |
+
progress (bool): Whether to show the download progress. Default: True.
|
| 77 |
+
file_name (str): The downloaded file name. If None, use the file name in the url. Default: None.
|
| 78 |
+
Returns:
|
| 79 |
+
str: The path to the downloaded file.
|
| 80 |
+
"""
|
| 81 |
+
if model_dir is None: # use the pytorch hub_dir
|
| 82 |
+
hub_dir = get_dir()
|
| 83 |
+
model_dir = os.path.join(hub_dir, 'checkpoints')
|
| 84 |
+
|
| 85 |
+
os.makedirs(model_dir, exist_ok=True)
|
| 86 |
+
|
| 87 |
+
parts = urlparse(url)
|
| 88 |
+
filename = os.path.basename(parts.path)
|
| 89 |
+
if file_name is not None:
|
| 90 |
+
filename = file_name
|
| 91 |
+
cached_file = os.path.abspath(os.path.join(model_dir, filename))
|
| 92 |
+
if not os.path.exists(cached_file):
|
| 93 |
+
print(f'Downloading: "{url}" to {cached_file}\n')
|
| 94 |
+
download_url_to_file(url, cached_file, hash_prefix=None, progress=progress)
|
| 95 |
+
return cached_file
|
basicsr/utils/file_client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified from https://github.com/open-mmlab/mmcv/blob/master/mmcv/fileio/file_client.py # noqa: E501
|
| 2 |
+
from abc import ABCMeta, abstractmethod
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class BaseStorageBackend(metaclass=ABCMeta):
|
| 6 |
+
"""Abstract class of storage backends.
|
| 7 |
+
|
| 8 |
+
All backends need to implement two apis: ``get()`` and ``get_text()``.
|
| 9 |
+
``get()`` reads the file as a byte stream and ``get_text()`` reads the file
|
| 10 |
+
as texts.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
@abstractmethod
|
| 14 |
+
def get(self, filepath):
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
@abstractmethod
|
| 18 |
+
def get_text(self, filepath):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class MemcachedBackend(BaseStorageBackend):
|
| 23 |
+
"""Memcached storage backend.
|
| 24 |
+
|
| 25 |
+
Attributes:
|
| 26 |
+
server_list_cfg (str): Config file for memcached server list.
|
| 27 |
+
client_cfg (str): Config file for memcached client.
|
| 28 |
+
sys_path (str | None): Additional path to be appended to `sys.path`.
|
| 29 |
+
Default: None.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, server_list_cfg, client_cfg, sys_path=None):
|
| 33 |
+
if sys_path is not None:
|
| 34 |
+
import sys
|
| 35 |
+
sys.path.append(sys_path)
|
| 36 |
+
try:
|
| 37 |
+
import mc
|
| 38 |
+
except ImportError:
|
| 39 |
+
raise ImportError('Please install memcached to enable MemcachedBackend.')
|
| 40 |
+
|
| 41 |
+
self.server_list_cfg = server_list_cfg
|
| 42 |
+
self.client_cfg = client_cfg
|
| 43 |
+
self._client = mc.MemcachedClient.GetInstance(self.server_list_cfg, self.client_cfg)
|
| 44 |
+
# mc.pyvector servers as a point which points to a memory cache
|
| 45 |
+
self._mc_buffer = mc.pyvector()
|
| 46 |
+
|
| 47 |
+
def get(self, filepath):
|
| 48 |
+
filepath = str(filepath)
|
| 49 |
+
import mc
|
| 50 |
+
self._client.Get(filepath, self._mc_buffer)
|
| 51 |
+
value_buf = mc.ConvertBuffer(self._mc_buffer)
|
| 52 |
+
return value_buf
|
| 53 |
+
|
| 54 |
+
def get_text(self, filepath):
|
| 55 |
+
raise NotImplementedError
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class HardDiskBackend(BaseStorageBackend):
|
| 59 |
+
"""Raw hard disks storage backend."""
|
| 60 |
+
|
| 61 |
+
def get(self, filepath):
|
| 62 |
+
filepath = str(filepath)
|
| 63 |
+
with open(filepath, 'rb') as f:
|
| 64 |
+
value_buf = f.read()
|
| 65 |
+
return value_buf
|
| 66 |
+
|
| 67 |
+
def get_text(self, filepath):
|
| 68 |
+
filepath = str(filepath)
|
| 69 |
+
with open(filepath, 'r') as f:
|
| 70 |
+
value_buf = f.read()
|
| 71 |
+
return value_buf
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class LmdbBackend(BaseStorageBackend):
|
| 75 |
+
"""Lmdb storage backend.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
db_paths (str | list[str]): Lmdb database paths.
|
| 79 |
+
client_keys (str | list[str]): Lmdb client keys. Default: 'default'.
|
| 80 |
+
readonly (bool, optional): Lmdb environment parameter. If True,
|
| 81 |
+
disallow any write operations. Default: True.
|
| 82 |
+
lock (bool, optional): Lmdb environment parameter. If False, when
|
| 83 |
+
concurrent access occurs, do not lock the database. Default: False.
|
| 84 |
+
readahead (bool, optional): Lmdb environment parameter. If False,
|
| 85 |
+
disable the OS filesystem readahead mechanism, which may improve
|
| 86 |
+
random read performance when a database is larger than RAM.
|
| 87 |
+
Default: False.
|
| 88 |
+
|
| 89 |
+
Attributes:
|
| 90 |
+
db_paths (list): Lmdb database path.
|
| 91 |
+
_client (list): A list of several lmdb envs.
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def __init__(self, db_paths, client_keys='default', readonly=True, lock=False, readahead=False, **kwargs):
|
| 95 |
+
try:
|
| 96 |
+
import lmdb
|
| 97 |
+
except ImportError:
|
| 98 |
+
raise ImportError('Please install lmdb to enable LmdbBackend.')
|
| 99 |
+
|
| 100 |
+
if isinstance(client_keys, str):
|
| 101 |
+
client_keys = [client_keys]
|
| 102 |
+
|
| 103 |
+
if isinstance(db_paths, list):
|
| 104 |
+
self.db_paths = [str(v) for v in db_paths]
|
| 105 |
+
elif isinstance(db_paths, str):
|
| 106 |
+
self.db_paths = [str(db_paths)]
|
| 107 |
+
assert len(client_keys) == len(self.db_paths), ('client_keys and db_paths should have the same length, '
|
| 108 |
+
f'but received {len(client_keys)} and {len(self.db_paths)}.')
|
| 109 |
+
|
| 110 |
+
self._client = {}
|
| 111 |
+
for client, path in zip(client_keys, self.db_paths):
|
| 112 |
+
self._client[client] = lmdb.open(path, readonly=readonly, lock=lock, readahead=readahead, **kwargs)
|
| 113 |
+
|
| 114 |
+
def get(self, filepath, client_key):
|
| 115 |
+
"""Get values according to the filepath from one lmdb named client_key.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
filepath (str | obj:`Path`): Here, filepath is the lmdb key.
|
| 119 |
+
client_key (str): Used for distinguishing differnet lmdb envs.
|
| 120 |
+
"""
|
| 121 |
+
filepath = str(filepath)
|
| 122 |
+
assert client_key in self._client, (f'client_key {client_key} is not ' 'in lmdb clients.')
|
| 123 |
+
client = self._client[client_key]
|
| 124 |
+
with client.begin(write=False) as txn:
|
| 125 |
+
value_buf = txn.get(filepath.encode('ascii'))
|
| 126 |
+
return value_buf
|
| 127 |
+
|
| 128 |
+
def get_text(self, filepath):
|
| 129 |
+
raise NotImplementedError
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class FileClient(object):
|
| 133 |
+
"""A general file client to access files in different backend.
|
| 134 |
+
|
| 135 |
+
The client loads a file or text in a specified backend from its path
|
| 136 |
+
and return it as a binary file. it can also register other backend
|
| 137 |
+
accessor with a given name and backend class.
|
| 138 |
+
|
| 139 |
+
Attributes:
|
| 140 |
+
backend (str): The storage backend type. Options are "disk",
|
| 141 |
+
"memcached" and "lmdb".
|
| 142 |
+
client (:obj:`BaseStorageBackend`): The backend object.
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
_backends = {
|
| 146 |
+
'disk': HardDiskBackend,
|
| 147 |
+
'memcached': MemcachedBackend,
|
| 148 |
+
'lmdb': LmdbBackend,
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
def __init__(self, backend='disk', **kwargs):
|
| 152 |
+
if backend not in self._backends:
|
| 153 |
+
raise ValueError(f'Backend {backend} is not supported. Currently supported ones'
|
| 154 |
+
f' are {list(self._backends.keys())}')
|
| 155 |
+
self.backend = backend
|
| 156 |
+
self.client = self._backends[backend](**kwargs)
|
| 157 |
+
|
| 158 |
+
def get(self, filepath, client_key='default'):
|
| 159 |
+
# client_key is used only for lmdb, where different fileclients have
|
| 160 |
+
# different lmdb environments.
|
| 161 |
+
if self.backend == 'lmdb':
|
| 162 |
+
return self.client.get(filepath, client_key)
|
| 163 |
+
else:
|
| 164 |
+
return self.client.get(filepath)
|
| 165 |
+
|
| 166 |
+
def get_text(self, filepath):
|
| 167 |
+
return self.client.get_text(filepath)
|
basicsr/utils/img_util.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import math
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import torch
|
| 6 |
+
from torchvision.utils import make_grid
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def img2tensor(imgs, bgr2rgb=True, float32=True):
|
| 10 |
+
"""Numpy array to tensor.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
imgs (list[ndarray] | ndarray): Input images.
|
| 14 |
+
bgr2rgb (bool): Whether to change bgr to rgb.
|
| 15 |
+
float32 (bool): Whether to change to float32.
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
list[tensor] | tensor: Tensor images. If returned results only have
|
| 19 |
+
one element, just return tensor.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def _totensor(img, bgr2rgb, float32):
|
| 23 |
+
if img.shape[2] == 3 and bgr2rgb:
|
| 24 |
+
if img.dtype == 'float64':
|
| 25 |
+
img = img.astype('float32')
|
| 26 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 27 |
+
img = torch.from_numpy(img.transpose(2, 0, 1))
|
| 28 |
+
if float32:
|
| 29 |
+
img = img.float()
|
| 30 |
+
return img
|
| 31 |
+
|
| 32 |
+
if isinstance(imgs, list):
|
| 33 |
+
return [_totensor(img, bgr2rgb, float32) for img in imgs]
|
| 34 |
+
else:
|
| 35 |
+
return _totensor(imgs, bgr2rgb, float32)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def tensor2img(tensor, rgb2bgr=True, out_type=np.uint8, min_max=(0, 1)):
|
| 39 |
+
"""Convert torch Tensors into image numpy arrays.
|
| 40 |
+
|
| 41 |
+
After clamping to [min, max], values will be normalized to [0, 1].
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
tensor (Tensor or list[Tensor]): Accept shapes:
|
| 45 |
+
1) 4D mini-batch Tensor of shape (B x 3/1 x H x W);
|
| 46 |
+
2) 3D Tensor of shape (3/1 x H x W);
|
| 47 |
+
3) 2D Tensor of shape (H x W).
|
| 48 |
+
Tensor channel should be in RGB order.
|
| 49 |
+
rgb2bgr (bool): Whether to change rgb to bgr.
|
| 50 |
+
out_type (numpy type): output types. If ``np.uint8``, transform outputs
|
| 51 |
+
to uint8 type with range [0, 255]; otherwise, float type with
|
| 52 |
+
range [0, 1]. Default: ``np.uint8``.
|
| 53 |
+
min_max (tuple[int]): min and max values for clamp.
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
(Tensor or list): 3D ndarray of shape (H x W x C) OR 2D ndarray of
|
| 57 |
+
shape (H x W). The channel order is BGR.
|
| 58 |
+
"""
|
| 59 |
+
if not (torch.is_tensor(tensor) or (isinstance(tensor, list) and all(torch.is_tensor(t) for t in tensor))):
|
| 60 |
+
raise TypeError(f'tensor or list of tensors expected, got {type(tensor)}')
|
| 61 |
+
|
| 62 |
+
if torch.is_tensor(tensor):
|
| 63 |
+
tensor = [tensor]
|
| 64 |
+
result = []
|
| 65 |
+
for _tensor in tensor:
|
| 66 |
+
_tensor = _tensor.squeeze(0).float().detach().cpu().clamp_(*min_max)
|
| 67 |
+
_tensor = (_tensor - min_max[0]) / (min_max[1] - min_max[0])
|
| 68 |
+
|
| 69 |
+
n_dim = _tensor.dim()
|
| 70 |
+
if n_dim == 4:
|
| 71 |
+
img_np = make_grid(_tensor, nrow=int(math.sqrt(_tensor.size(0))), normalize=False).numpy()
|
| 72 |
+
img_np = img_np.transpose(1, 2, 0)
|
| 73 |
+
if rgb2bgr:
|
| 74 |
+
img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
| 75 |
+
elif n_dim == 3:
|
| 76 |
+
img_np = _tensor.numpy()
|
| 77 |
+
img_np = img_np.transpose(1, 2, 0)
|
| 78 |
+
if img_np.shape[2] == 1: # gray image
|
| 79 |
+
img_np = np.squeeze(img_np, axis=2)
|
| 80 |
+
else:
|
| 81 |
+
if rgb2bgr:
|
| 82 |
+
img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
| 83 |
+
elif n_dim == 2:
|
| 84 |
+
img_np = _tensor.numpy()
|
| 85 |
+
else:
|
| 86 |
+
raise TypeError('Only support 4D, 3D or 2D tensor. ' f'But received with dimension: {n_dim}')
|
| 87 |
+
if out_type == np.uint8:
|
| 88 |
+
# Unlike MATLAB, numpy.unit8() WILL NOT round by default.
|
| 89 |
+
img_np = (img_np * 255.0).round()
|
| 90 |
+
img_np = img_np.astype(out_type)
|
| 91 |
+
result.append(img_np)
|
| 92 |
+
if len(result) == 1:
|
| 93 |
+
result = result[0]
|
| 94 |
+
return result
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def tensor2img_fast(tensor, rgb2bgr=True, min_max=(0, 1)):
|
| 98 |
+
"""This implementation is slightly faster than tensor2img.
|
| 99 |
+
It now only supports torch tensor with shape (1, c, h, w).
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
tensor (Tensor): Now only support torch tensor with (1, c, h, w).
|
| 103 |
+
rgb2bgr (bool): Whether to change rgb to bgr. Default: True.
|
| 104 |
+
min_max (tuple[int]): min and max values for clamp.
|
| 105 |
+
"""
|
| 106 |
+
output = tensor.squeeze(0).detach().clamp_(*min_max).permute(1, 2, 0)
|
| 107 |
+
output = (output - min_max[0]) / (min_max[1] - min_max[0]) * 255
|
| 108 |
+
output = output.type(torch.uint8).cpu().numpy()
|
| 109 |
+
if rgb2bgr:
|
| 110 |
+
output = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
|
| 111 |
+
return output
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def imfrombytes(content, flag='color', float32=False):
|
| 115 |
+
"""Read an image from bytes.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
content (bytes): Image bytes got from files or other streams.
|
| 119 |
+
flag (str): Flags specifying the color type of a loaded image,
|
| 120 |
+
candidates are `color`, `grayscale` and `unchanged`.
|
| 121 |
+
float32 (bool): Whether to change to float32., If True, will also norm
|
| 122 |
+
to [0, 1]. Default: False.
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
ndarray: Loaded image array.
|
| 126 |
+
"""
|
| 127 |
+
img_np = np.frombuffer(content, np.uint8)
|
| 128 |
+
imread_flags = {'color': cv2.IMREAD_COLOR, 'grayscale': cv2.IMREAD_GRAYSCALE, 'unchanged': cv2.IMREAD_UNCHANGED}
|
| 129 |
+
img = cv2.imdecode(img_np, imread_flags[flag])
|
| 130 |
+
if float32:
|
| 131 |
+
img = img.astype(np.float32) / 255.
|
| 132 |
+
return img
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def imwrite(img, file_path, params=None, auto_mkdir=True):
|
| 136 |
+
"""Write image to file.
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
img (ndarray): Image array to be written.
|
| 140 |
+
file_path (str): Image file path.
|
| 141 |
+
params (None or list): Same as opencv's :func:`imwrite` interface.
|
| 142 |
+
auto_mkdir (bool): If the parent folder of `file_path` does not exist,
|
| 143 |
+
whether to create it automatically.
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
bool: Successful or not.
|
| 147 |
+
"""
|
| 148 |
+
if auto_mkdir:
|
| 149 |
+
dir_name = os.path.abspath(os.path.dirname(file_path))
|
| 150 |
+
os.makedirs(dir_name, exist_ok=True)
|
| 151 |
+
return cv2.imwrite(file_path, img, params)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def crop_border(imgs, crop_border):
|
| 155 |
+
"""Crop borders of images.
|
| 156 |
+
|
| 157 |
+
Args:
|
| 158 |
+
imgs (list[ndarray] | ndarray): Images with shape (h, w, c).
|
| 159 |
+
crop_border (int): Crop border for each end of height and weight.
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
list[ndarray]: Cropped images.
|
| 163 |
+
"""
|
| 164 |
+
if crop_border == 0:
|
| 165 |
+
return imgs
|
| 166 |
+
else:
|
| 167 |
+
if isinstance(imgs, list):
|
| 168 |
+
return [v[crop_border:-crop_border, crop_border:-crop_border, ...] for v in imgs]
|
| 169 |
+
else:
|
| 170 |
+
return imgs[crop_border:-crop_border, crop_border:-crop_border, ...]
|
| 171 |
+
|
basicsr/utils/lmdb_util.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import lmdb
|
| 3 |
+
import sys
|
| 4 |
+
from multiprocessing import Pool
|
| 5 |
+
from os import path as osp
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def make_lmdb_from_imgs(data_path,
|
| 10 |
+
lmdb_path,
|
| 11 |
+
img_path_list,
|
| 12 |
+
keys,
|
| 13 |
+
batch=5000,
|
| 14 |
+
compress_level=1,
|
| 15 |
+
multiprocessing_read=False,
|
| 16 |
+
n_thread=40,
|
| 17 |
+
map_size=None):
|
| 18 |
+
"""Make lmdb from images.
|
| 19 |
+
|
| 20 |
+
Contents of lmdb. The file structure is:
|
| 21 |
+
example.lmdb
|
| 22 |
+
├── data.mdb
|
| 23 |
+
├── lock.mdb
|
| 24 |
+
├── meta_info.txt
|
| 25 |
+
|
| 26 |
+
The data.mdb and lock.mdb are standard lmdb files and you can refer to
|
| 27 |
+
https://lmdb.readthedocs.io/en/release/ for more details.
|
| 28 |
+
|
| 29 |
+
The meta_info.txt is a specified txt file to record the meta information
|
| 30 |
+
of our datasets. It will be automatically created when preparing
|
| 31 |
+
datasets by our provided dataset tools.
|
| 32 |
+
Each line in the txt file records 1)image name (with extension),
|
| 33 |
+
2)image shape, and 3)compression level, separated by a white space.
|
| 34 |
+
|
| 35 |
+
For example, the meta information could be:
|
| 36 |
+
`000_00000000.png (720,1280,3) 1`, which means:
|
| 37 |
+
1) image name (with extension): 000_00000000.png;
|
| 38 |
+
2) image shape: (720,1280,3);
|
| 39 |
+
3) compression level: 1
|
| 40 |
+
|
| 41 |
+
We use the image name without extension as the lmdb key.
|
| 42 |
+
|
| 43 |
+
If `multiprocessing_read` is True, it will read all the images to memory
|
| 44 |
+
using multiprocessing. Thus, your server needs to have enough memory.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
data_path (str): Data path for reading images.
|
| 48 |
+
lmdb_path (str): Lmdb save path.
|
| 49 |
+
img_path_list (str): Image path list.
|
| 50 |
+
keys (str): Used for lmdb keys.
|
| 51 |
+
batch (int): After processing batch images, lmdb commits.
|
| 52 |
+
Default: 5000.
|
| 53 |
+
compress_level (int): Compress level when encoding images. Default: 1.
|
| 54 |
+
multiprocessing_read (bool): Whether use multiprocessing to read all
|
| 55 |
+
the images to memory. Default: False.
|
| 56 |
+
n_thread (int): For multiprocessing.
|
| 57 |
+
map_size (int | None): Map size for lmdb env. If None, use the
|
| 58 |
+
estimated size from images. Default: None
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
assert len(img_path_list) == len(keys), ('img_path_list and keys should have the same length, '
|
| 62 |
+
f'but got {len(img_path_list)} and {len(keys)}')
|
| 63 |
+
print(f'Create lmdb for {data_path}, save to {lmdb_path}...')
|
| 64 |
+
print(f'Totoal images: {len(img_path_list)}')
|
| 65 |
+
if not lmdb_path.endswith('.lmdb'):
|
| 66 |
+
raise ValueError("lmdb_path must end with '.lmdb'.")
|
| 67 |
+
if osp.exists(lmdb_path):
|
| 68 |
+
print(f'Folder {lmdb_path} already exists. Exit.')
|
| 69 |
+
sys.exit(1)
|
| 70 |
+
|
| 71 |
+
if multiprocessing_read:
|
| 72 |
+
# read all the images to memory (multiprocessing)
|
| 73 |
+
dataset = {} # use dict to keep the order for multiprocessing
|
| 74 |
+
shapes = {}
|
| 75 |
+
print(f'Read images with multiprocessing, #thread: {n_thread} ...')
|
| 76 |
+
pbar = tqdm(total=len(img_path_list), unit='image')
|
| 77 |
+
|
| 78 |
+
def callback(arg):
|
| 79 |
+
"""get the image data and update pbar."""
|
| 80 |
+
key, dataset[key], shapes[key] = arg
|
| 81 |
+
pbar.update(1)
|
| 82 |
+
pbar.set_description(f'Read {key}')
|
| 83 |
+
|
| 84 |
+
pool = Pool(n_thread)
|
| 85 |
+
for path, key in zip(img_path_list, keys):
|
| 86 |
+
pool.apply_async(read_img_worker, args=(osp.join(data_path, path), key, compress_level), callback=callback)
|
| 87 |
+
pool.close()
|
| 88 |
+
pool.join()
|
| 89 |
+
pbar.close()
|
| 90 |
+
print(f'Finish reading {len(img_path_list)} images.')
|
| 91 |
+
|
| 92 |
+
# create lmdb environment
|
| 93 |
+
if map_size is None:
|
| 94 |
+
# obtain data size for one image
|
| 95 |
+
img = cv2.imread(osp.join(data_path, img_path_list[0]), cv2.IMREAD_UNCHANGED)
|
| 96 |
+
_, img_byte = cv2.imencode('.png', img, [cv2.IMWRITE_PNG_COMPRESSION, compress_level])
|
| 97 |
+
data_size_per_img = img_byte.nbytes
|
| 98 |
+
print('Data size per image is: ', data_size_per_img)
|
| 99 |
+
data_size = data_size_per_img * len(img_path_list)
|
| 100 |
+
map_size = data_size * 10
|
| 101 |
+
|
| 102 |
+
env = lmdb.open(lmdb_path, map_size=map_size)
|
| 103 |
+
|
| 104 |
+
# write data to lmdb
|
| 105 |
+
pbar = tqdm(total=len(img_path_list), unit='chunk')
|
| 106 |
+
txn = env.begin(write=True)
|
| 107 |
+
txt_file = open(osp.join(lmdb_path, 'meta_info.txt'), 'w')
|
| 108 |
+
for idx, (path, key) in enumerate(zip(img_path_list, keys)):
|
| 109 |
+
pbar.update(1)
|
| 110 |
+
pbar.set_description(f'Write {key}')
|
| 111 |
+
key_byte = key.encode('ascii')
|
| 112 |
+
if multiprocessing_read:
|
| 113 |
+
img_byte = dataset[key]
|
| 114 |
+
h, w, c = shapes[key]
|
| 115 |
+
else:
|
| 116 |
+
_, img_byte, img_shape = read_img_worker(osp.join(data_path, path), key, compress_level)
|
| 117 |
+
h, w, c = img_shape
|
| 118 |
+
|
| 119 |
+
txn.put(key_byte, img_byte)
|
| 120 |
+
# write meta information
|
| 121 |
+
txt_file.write(f'{key}.png ({h},{w},{c}) {compress_level}\n')
|
| 122 |
+
if idx % batch == 0:
|
| 123 |
+
txn.commit()
|
| 124 |
+
txn = env.begin(write=True)
|
| 125 |
+
pbar.close()
|
| 126 |
+
txn.commit()
|
| 127 |
+
env.close()
|
| 128 |
+
txt_file.close()
|
| 129 |
+
print('\nFinish writing lmdb.')
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def read_img_worker(path, key, compress_level):
|
| 133 |
+
"""Read image worker.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
path (str): Image path.
|
| 137 |
+
key (str): Image key.
|
| 138 |
+
compress_level (int): Compress level when encoding images.
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
str: Image key.
|
| 142 |
+
byte: Image byte.
|
| 143 |
+
tuple[int]: Image shape.
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
| 147 |
+
if img.ndim == 2:
|
| 148 |
+
h, w = img.shape
|
| 149 |
+
c = 1
|
| 150 |
+
else:
|
| 151 |
+
h, w, c = img.shape
|
| 152 |
+
_, img_byte = cv2.imencode('.png', img, [cv2.IMWRITE_PNG_COMPRESSION, compress_level])
|
| 153 |
+
return (key, img_byte, (h, w, c))
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class LmdbMaker():
|
| 157 |
+
"""LMDB Maker.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
lmdb_path (str): Lmdb save path.
|
| 161 |
+
map_size (int): Map size for lmdb env. Default: 1024 ** 4, 1TB.
|
| 162 |
+
batch (int): After processing batch images, lmdb commits.
|
| 163 |
+
Default: 5000.
|
| 164 |
+
compress_level (int): Compress level when encoding images. Default: 1.
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
def __init__(self, lmdb_path, map_size=1024**4, batch=5000, compress_level=1):
|
| 168 |
+
if not lmdb_path.endswith('.lmdb'):
|
| 169 |
+
raise ValueError("lmdb_path must end with '.lmdb'.")
|
| 170 |
+
if osp.exists(lmdb_path):
|
| 171 |
+
print(f'Folder {lmdb_path} already exists. Exit.')
|
| 172 |
+
sys.exit(1)
|
| 173 |
+
|
| 174 |
+
self.lmdb_path = lmdb_path
|
| 175 |
+
self.batch = batch
|
| 176 |
+
self.compress_level = compress_level
|
| 177 |
+
self.env = lmdb.open(lmdb_path, map_size=map_size)
|
| 178 |
+
self.txn = self.env.begin(write=True)
|
| 179 |
+
self.txt_file = open(osp.join(lmdb_path, 'meta_info.txt'), 'w')
|
| 180 |
+
self.counter = 0
|
| 181 |
+
|
| 182 |
+
def put(self, img_byte, key, img_shape):
|
| 183 |
+
self.counter += 1
|
| 184 |
+
key_byte = key.encode('ascii')
|
| 185 |
+
self.txn.put(key_byte, img_byte)
|
| 186 |
+
# write meta information
|
| 187 |
+
h, w, c = img_shape
|
| 188 |
+
self.txt_file.write(f'{key}.png ({h},{w},{c}) {self.compress_level}\n')
|
| 189 |
+
if self.counter % self.batch == 0:
|
| 190 |
+
self.txn.commit()
|
| 191 |
+
self.txn = self.env.begin(write=True)
|
| 192 |
+
|
| 193 |
+
def close(self):
|
| 194 |
+
self.txn.commit()
|
| 195 |
+
self.env.close()
|
| 196 |
+
self.txt_file.close()
|
basicsr/utils/logger.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import logging
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from .dist_util import get_dist_info, master_only
|
| 6 |
+
|
| 7 |
+
initialized_logger = {}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class MessageLogger():
|
| 11 |
+
"""Message logger for printing.
|
| 12 |
+
Args:
|
| 13 |
+
opt (dict): Config. It contains the following keys:
|
| 14 |
+
name (str): Exp name.
|
| 15 |
+
logger (dict): Contains 'print_freq' (str) for logger interval.
|
| 16 |
+
train (dict): Contains 'total_iter' (int) for total iters.
|
| 17 |
+
use_tb_logger (bool): Use tensorboard logger.
|
| 18 |
+
start_iter (int): Start iter. Default: 1.
|
| 19 |
+
tb_logger (obj:`tb_logger`): Tensorboard logger. Default: None.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, opt, start_iter=1, tb_logger=None):
|
| 23 |
+
self.exp_name = opt['name']
|
| 24 |
+
self.interval = opt['logger']['print_freq']
|
| 25 |
+
self.start_iter = start_iter
|
| 26 |
+
self.max_iters = opt['train']['total_iter']
|
| 27 |
+
self.use_tb_logger = opt['logger']['use_tb_logger']
|
| 28 |
+
self.tb_logger = tb_logger
|
| 29 |
+
self.start_time = time.time()
|
| 30 |
+
self.logger = get_root_logger()
|
| 31 |
+
|
| 32 |
+
@master_only
|
| 33 |
+
def __call__(self, log_vars):
|
| 34 |
+
"""Format logging message.
|
| 35 |
+
Args:
|
| 36 |
+
log_vars (dict): It contains the following keys:
|
| 37 |
+
epoch (int): Epoch number.
|
| 38 |
+
iter (int): Current iter.
|
| 39 |
+
lrs (list): List for learning rates.
|
| 40 |
+
time (float): Iter time.
|
| 41 |
+
data_time (float): Data time for each iter.
|
| 42 |
+
"""
|
| 43 |
+
# epoch, iter, learning rates
|
| 44 |
+
epoch = log_vars.pop('epoch')
|
| 45 |
+
current_iter = log_vars.pop('iter')
|
| 46 |
+
lrs = log_vars.pop('lrs')
|
| 47 |
+
|
| 48 |
+
message = (f'[{self.exp_name[:5]}..][epoch:{epoch:3d}, ' f'iter:{current_iter:8,d}, lr:(')
|
| 49 |
+
for v in lrs:
|
| 50 |
+
message += f'{v:.3e},'
|
| 51 |
+
message += ')] '
|
| 52 |
+
|
| 53 |
+
# time and estimated time
|
| 54 |
+
if 'time' in log_vars.keys():
|
| 55 |
+
iter_time = log_vars.pop('time')
|
| 56 |
+
data_time = log_vars.pop('data_time')
|
| 57 |
+
|
| 58 |
+
total_time = time.time() - self.start_time
|
| 59 |
+
time_sec_avg = total_time / (current_iter - self.start_iter + 1)
|
| 60 |
+
eta_sec = time_sec_avg * (self.max_iters - current_iter - 1)
|
| 61 |
+
eta_str = str(datetime.timedelta(seconds=int(eta_sec)))
|
| 62 |
+
message += f'[eta: {eta_str}, '
|
| 63 |
+
message += f'time (data): {iter_time:.3f} ({data_time:.3f})] '
|
| 64 |
+
|
| 65 |
+
# other items, especially losses
|
| 66 |
+
for k, v in log_vars.items():
|
| 67 |
+
message += f'{k}: {v:.4e} '
|
| 68 |
+
# tensorboard logger
|
| 69 |
+
if self.use_tb_logger:
|
| 70 |
+
# if k.startswith('l_'):
|
| 71 |
+
# self.tb_logger.add_scalar(f'losses/{k}', v, current_iter)
|
| 72 |
+
# else:
|
| 73 |
+
self.tb_logger.add_scalar(k, v, current_iter)
|
| 74 |
+
self.logger.info(message)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@master_only
|
| 78 |
+
def init_tb_logger(log_dir):
|
| 79 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 80 |
+
tb_logger = SummaryWriter(log_dir=log_dir)
|
| 81 |
+
return tb_logger
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@master_only
|
| 85 |
+
def init_wandb_logger(opt):
|
| 86 |
+
"""We now only use wandb to sync tensorboard log."""
|
| 87 |
+
import wandb
|
| 88 |
+
logger = logging.getLogger('basicsr')
|
| 89 |
+
|
| 90 |
+
project = opt['logger']['wandb']['project']
|
| 91 |
+
resume_id = opt['logger']['wandb'].get('resume_id')
|
| 92 |
+
if resume_id:
|
| 93 |
+
wandb_id = resume_id
|
| 94 |
+
resume = 'allow'
|
| 95 |
+
logger.warning(f'Resume wandb logger with id={wandb_id}.')
|
| 96 |
+
else:
|
| 97 |
+
wandb_id = wandb.util.generate_id()
|
| 98 |
+
resume = 'never'
|
| 99 |
+
|
| 100 |
+
wandb.init(id=wandb_id, resume=resume, name=opt['name'], config=opt, project=project, sync_tensorboard=True)
|
| 101 |
+
|
| 102 |
+
logger.info(f'Use wandb logger with id={wandb_id}; project={project}.')
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def get_root_logger(logger_name='basicsr', log_level=logging.INFO, log_file=None):
|
| 106 |
+
"""Get the root logger.
|
| 107 |
+
The logger will be initialized if it has not been initialized. By default a
|
| 108 |
+
StreamHandler will be added. If `log_file` is specified, a FileHandler will
|
| 109 |
+
also be added.
|
| 110 |
+
Args:
|
| 111 |
+
logger_name (str): root logger name. Default: 'basicsr'.
|
| 112 |
+
log_file (str | None): The log filename. If specified, a FileHandler
|
| 113 |
+
will be added to the root logger.
|
| 114 |
+
log_level (int): The root logger level. Note that only the process of
|
| 115 |
+
rank 0 is affected, while other processes will set the level to
|
| 116 |
+
"Error" and be silent most of the time.
|
| 117 |
+
Returns:
|
| 118 |
+
logging.Logger: The root logger.
|
| 119 |
+
"""
|
| 120 |
+
logger = logging.getLogger(logger_name)
|
| 121 |
+
# if the logger has been initialized, just return it
|
| 122 |
+
if logger_name in initialized_logger:
|
| 123 |
+
return logger
|
| 124 |
+
|
| 125 |
+
format_str = '%(asctime)s %(levelname)s: %(message)s'
|
| 126 |
+
stream_handler = logging.StreamHandler()
|
| 127 |
+
stream_handler.setFormatter(logging.Formatter(format_str))
|
| 128 |
+
logger.addHandler(stream_handler)
|
| 129 |
+
logger.propagate = False
|
| 130 |
+
rank, _ = get_dist_info()
|
| 131 |
+
if rank != 0:
|
| 132 |
+
logger.setLevel('ERROR')
|
| 133 |
+
elif log_file is not None:
|
| 134 |
+
logger.setLevel(log_level)
|
| 135 |
+
# add file handler
|
| 136 |
+
# file_handler = logging.FileHandler(log_file, 'w')
|
| 137 |
+
file_handler = logging.FileHandler(log_file, 'a') #Shangchen: keep the previous log
|
| 138 |
+
file_handler.setFormatter(logging.Formatter(format_str))
|
| 139 |
+
file_handler.setLevel(log_level)
|
| 140 |
+
logger.addHandler(file_handler)
|
| 141 |
+
initialized_logger[logger_name] = True
|
| 142 |
+
return logger
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def get_env_info():
|
| 146 |
+
"""Get environment information.
|
| 147 |
+
Currently, only log the software version.
|
| 148 |
+
"""
|
| 149 |
+
import torch
|
| 150 |
+
import torchvision
|
| 151 |
+
|
| 152 |
+
from basicsr.version import __version__
|
| 153 |
+
msg = r"""
|
| 154 |
+
____ _ _____ ____
|
| 155 |
+
/ __ ) ____ _ _____ (_)_____/ ___/ / __ \
|
| 156 |
+
/ __ |/ __ `// ___// // ___/\__ \ / /_/ /
|
| 157 |
+
/ /_/ // /_/ /(__ )/ // /__ ___/ // _, _/
|
| 158 |
+
/_____/ \__,_//____//_/ \___//____//_/ |_|
|
| 159 |
+
______ __ __ __ __
|
| 160 |
+
/ ____/____ ____ ____/ / / / __ __ _____ / /__ / /
|
| 161 |
+
/ / __ / __ \ / __ \ / __ / / / / / / // ___// //_/ / /
|
| 162 |
+
/ /_/ // /_/ // /_/ // /_/ / / /___/ /_/ // /__ / /< /_/
|
| 163 |
+
\____/ \____/ \____/ \____/ /_____/\____/ \___//_/|_| (_)
|
| 164 |
+
"""
|
| 165 |
+
msg += ('\nVersion Information: '
|
| 166 |
+
f'\n\tBasicSR: {__version__}'
|
| 167 |
+
f'\n\tPyTorch: {torch.__version__}'
|
| 168 |
+
f'\n\tTorchVision: {torchvision.__version__}')
|
| 169 |
+
return msg
|
basicsr/utils/matlab_functions.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def cubic(x):
|
| 7 |
+
"""cubic function used for calculate_weights_indices."""
|
| 8 |
+
absx = torch.abs(x)
|
| 9 |
+
absx2 = absx**2
|
| 10 |
+
absx3 = absx**3
|
| 11 |
+
return (1.5 * absx3 - 2.5 * absx2 + 1) * (
|
| 12 |
+
(absx <= 1).type_as(absx)) + (-0.5 * absx3 + 2.5 * absx2 - 4 * absx + 2) * (((absx > 1) *
|
| 13 |
+
(absx <= 2)).type_as(absx))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def calculate_weights_indices(in_length, out_length, scale, kernel, kernel_width, antialiasing):
|
| 17 |
+
"""Calculate weights and indices, used for imresize function.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
in_length (int): Input length.
|
| 21 |
+
out_length (int): Output length.
|
| 22 |
+
scale (float): Scale factor.
|
| 23 |
+
kernel_width (int): Kernel width.
|
| 24 |
+
antialisaing (bool): Whether to apply anti-aliasing when downsampling.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
if (scale < 1) and antialiasing:
|
| 28 |
+
# Use a modified kernel (larger kernel width) to simultaneously
|
| 29 |
+
# interpolate and antialias
|
| 30 |
+
kernel_width = kernel_width / scale
|
| 31 |
+
|
| 32 |
+
# Output-space coordinates
|
| 33 |
+
x = torch.linspace(1, out_length, out_length)
|
| 34 |
+
|
| 35 |
+
# Input-space coordinates. Calculate the inverse mapping such that 0.5
|
| 36 |
+
# in output space maps to 0.5 in input space, and 0.5 + scale in output
|
| 37 |
+
# space maps to 1.5 in input space.
|
| 38 |
+
u = x / scale + 0.5 * (1 - 1 / scale)
|
| 39 |
+
|
| 40 |
+
# What is the left-most pixel that can be involved in the computation?
|
| 41 |
+
left = torch.floor(u - kernel_width / 2)
|
| 42 |
+
|
| 43 |
+
# What is the maximum number of pixels that can be involved in the
|
| 44 |
+
# computation? Note: it's OK to use an extra pixel here; if the
|
| 45 |
+
# corresponding weights are all zero, it will be eliminated at the end
|
| 46 |
+
# of this function.
|
| 47 |
+
p = math.ceil(kernel_width) + 2
|
| 48 |
+
|
| 49 |
+
# The indices of the input pixels involved in computing the k-th output
|
| 50 |
+
# pixel are in row k of the indices matrix.
|
| 51 |
+
indices = left.view(out_length, 1).expand(out_length, p) + torch.linspace(0, p - 1, p).view(1, p).expand(
|
| 52 |
+
out_length, p)
|
| 53 |
+
|
| 54 |
+
# The weights used to compute the k-th output pixel are in row k of the
|
| 55 |
+
# weights matrix.
|
| 56 |
+
distance_to_center = u.view(out_length, 1).expand(out_length, p) - indices
|
| 57 |
+
|
| 58 |
+
# apply cubic kernel
|
| 59 |
+
if (scale < 1) and antialiasing:
|
| 60 |
+
weights = scale * cubic(distance_to_center * scale)
|
| 61 |
+
else:
|
| 62 |
+
weights = cubic(distance_to_center)
|
| 63 |
+
|
| 64 |
+
# Normalize the weights matrix so that each row sums to 1.
|
| 65 |
+
weights_sum = torch.sum(weights, 1).view(out_length, 1)
|
| 66 |
+
weights = weights / weights_sum.expand(out_length, p)
|
| 67 |
+
|
| 68 |
+
# If a column in weights is all zero, get rid of it. only consider the
|
| 69 |
+
# first and last column.
|
| 70 |
+
weights_zero_tmp = torch.sum((weights == 0), 0)
|
| 71 |
+
if not math.isclose(weights_zero_tmp[0], 0, rel_tol=1e-6):
|
| 72 |
+
indices = indices.narrow(1, 1, p - 2)
|
| 73 |
+
weights = weights.narrow(1, 1, p - 2)
|
| 74 |
+
if not math.isclose(weights_zero_tmp[-1], 0, rel_tol=1e-6):
|
| 75 |
+
indices = indices.narrow(1, 0, p - 2)
|
| 76 |
+
weights = weights.narrow(1, 0, p - 2)
|
| 77 |
+
weights = weights.contiguous()
|
| 78 |
+
indices = indices.contiguous()
|
| 79 |
+
sym_len_s = -indices.min() + 1
|
| 80 |
+
sym_len_e = indices.max() - in_length
|
| 81 |
+
indices = indices + sym_len_s - 1
|
| 82 |
+
return weights, indices, int(sym_len_s), int(sym_len_e)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@torch.no_grad()
|
| 86 |
+
def imresize(img, scale, antialiasing=True):
|
| 87 |
+
"""imresize function same as MATLAB.
|
| 88 |
+
|
| 89 |
+
It now only supports bicubic.
|
| 90 |
+
The same scale applies for both height and width.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
img (Tensor | Numpy array):
|
| 94 |
+
Tensor: Input image with shape (c, h, w), [0, 1] range.
|
| 95 |
+
Numpy: Input image with shape (h, w, c), [0, 1] range.
|
| 96 |
+
scale (float): Scale factor. The same scale applies for both height
|
| 97 |
+
and width.
|
| 98 |
+
antialisaing (bool): Whether to apply anti-aliasing when downsampling.
|
| 99 |
+
Default: True.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Tensor: Output image with shape (c, h, w), [0, 1] range, w/o round.
|
| 103 |
+
"""
|
| 104 |
+
if type(img).__module__ == np.__name__: # numpy type
|
| 105 |
+
numpy_type = True
|
| 106 |
+
img = torch.from_numpy(img.transpose(2, 0, 1)).float()
|
| 107 |
+
else:
|
| 108 |
+
numpy_type = False
|
| 109 |
+
|
| 110 |
+
in_c, in_h, in_w = img.size()
|
| 111 |
+
out_h, out_w = math.ceil(in_h * scale), math.ceil(in_w * scale)
|
| 112 |
+
kernel_width = 4
|
| 113 |
+
kernel = 'cubic'
|
| 114 |
+
|
| 115 |
+
# get weights and indices
|
| 116 |
+
weights_h, indices_h, sym_len_hs, sym_len_he = calculate_weights_indices(in_h, out_h, scale, kernel, kernel_width,
|
| 117 |
+
antialiasing)
|
| 118 |
+
weights_w, indices_w, sym_len_ws, sym_len_we = calculate_weights_indices(in_w, out_w, scale, kernel, kernel_width,
|
| 119 |
+
antialiasing)
|
| 120 |
+
# process H dimension
|
| 121 |
+
# symmetric copying
|
| 122 |
+
img_aug = torch.FloatTensor(in_c, in_h + sym_len_hs + sym_len_he, in_w)
|
| 123 |
+
img_aug.narrow(1, sym_len_hs, in_h).copy_(img)
|
| 124 |
+
|
| 125 |
+
sym_patch = img[:, :sym_len_hs, :]
|
| 126 |
+
inv_idx = torch.arange(sym_patch.size(1) - 1, -1, -1).long()
|
| 127 |
+
sym_patch_inv = sym_patch.index_select(1, inv_idx)
|
| 128 |
+
img_aug.narrow(1, 0, sym_len_hs).copy_(sym_patch_inv)
|
| 129 |
+
|
| 130 |
+
sym_patch = img[:, -sym_len_he:, :]
|
| 131 |
+
inv_idx = torch.arange(sym_patch.size(1) - 1, -1, -1).long()
|
| 132 |
+
sym_patch_inv = sym_patch.index_select(1, inv_idx)
|
| 133 |
+
img_aug.narrow(1, sym_len_hs + in_h, sym_len_he).copy_(sym_patch_inv)
|
| 134 |
+
|
| 135 |
+
out_1 = torch.FloatTensor(in_c, out_h, in_w)
|
| 136 |
+
kernel_width = weights_h.size(1)
|
| 137 |
+
for i in range(out_h):
|
| 138 |
+
idx = int(indices_h[i][0])
|
| 139 |
+
for j in range(in_c):
|
| 140 |
+
out_1[j, i, :] = img_aug[j, idx:idx + kernel_width, :].transpose(0, 1).mv(weights_h[i])
|
| 141 |
+
|
| 142 |
+
# process W dimension
|
| 143 |
+
# symmetric copying
|
| 144 |
+
out_1_aug = torch.FloatTensor(in_c, out_h, in_w + sym_len_ws + sym_len_we)
|
| 145 |
+
out_1_aug.narrow(2, sym_len_ws, in_w).copy_(out_1)
|
| 146 |
+
|
| 147 |
+
sym_patch = out_1[:, :, :sym_len_ws]
|
| 148 |
+
inv_idx = torch.arange(sym_patch.size(2) - 1, -1, -1).long()
|
| 149 |
+
sym_patch_inv = sym_patch.index_select(2, inv_idx)
|
| 150 |
+
out_1_aug.narrow(2, 0, sym_len_ws).copy_(sym_patch_inv)
|
| 151 |
+
|
| 152 |
+
sym_patch = out_1[:, :, -sym_len_we:]
|
| 153 |
+
inv_idx = torch.arange(sym_patch.size(2) - 1, -1, -1).long()
|
| 154 |
+
sym_patch_inv = sym_patch.index_select(2, inv_idx)
|
| 155 |
+
out_1_aug.narrow(2, sym_len_ws + in_w, sym_len_we).copy_(sym_patch_inv)
|
| 156 |
+
|
| 157 |
+
out_2 = torch.FloatTensor(in_c, out_h, out_w)
|
| 158 |
+
kernel_width = weights_w.size(1)
|
| 159 |
+
for i in range(out_w):
|
| 160 |
+
idx = int(indices_w[i][0])
|
| 161 |
+
for j in range(in_c):
|
| 162 |
+
out_2[j, :, i] = out_1_aug[j, :, idx:idx + kernel_width].mv(weights_w[i])
|
| 163 |
+
|
| 164 |
+
if numpy_type:
|
| 165 |
+
out_2 = out_2.numpy().transpose(1, 2, 0)
|
| 166 |
+
return out_2
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def rgb2ycbcr(img, y_only=False):
|
| 170 |
+
"""Convert a RGB image to YCbCr image.
|
| 171 |
+
|
| 172 |
+
This function produces the same results as Matlab's `rgb2ycbcr` function.
|
| 173 |
+
It implements the ITU-R BT.601 conversion for standard-definition
|
| 174 |
+
television. See more details in
|
| 175 |
+
https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion.
|
| 176 |
+
|
| 177 |
+
It differs from a similar function in cv2.cvtColor: `RGB <-> YCrCb`.
|
| 178 |
+
In OpenCV, it implements a JPEG conversion. See more details in
|
| 179 |
+
https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
img (ndarray): The input image. It accepts:
|
| 183 |
+
1. np.uint8 type with range [0, 255];
|
| 184 |
+
2. np.float32 type with range [0, 1].
|
| 185 |
+
y_only (bool): Whether to only return Y channel. Default: False.
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
ndarray: The converted YCbCr image. The output image has the same type
|
| 189 |
+
and range as input image.
|
| 190 |
+
"""
|
| 191 |
+
img_type = img.dtype
|
| 192 |
+
img = _convert_input_type_range(img)
|
| 193 |
+
if y_only:
|
| 194 |
+
out_img = np.dot(img, [65.481, 128.553, 24.966]) + 16.0
|
| 195 |
+
else:
|
| 196 |
+
out_img = np.matmul(
|
| 197 |
+
img, [[65.481, -37.797, 112.0], [128.553, -74.203, -93.786], [24.966, 112.0, -18.214]]) + [16, 128, 128]
|
| 198 |
+
out_img = _convert_output_type_range(out_img, img_type)
|
| 199 |
+
return out_img
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def bgr2ycbcr(img, y_only=False):
|
| 203 |
+
"""Convert a BGR image to YCbCr image.
|
| 204 |
+
|
| 205 |
+
The bgr version of rgb2ycbcr.
|
| 206 |
+
It implements the ITU-R BT.601 conversion for standard-definition
|
| 207 |
+
television. See more details in
|
| 208 |
+
https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion.
|
| 209 |
+
|
| 210 |
+
It differs from a similar function in cv2.cvtColor: `BGR <-> YCrCb`.
|
| 211 |
+
In OpenCV, it implements a JPEG conversion. See more details in
|
| 212 |
+
https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
img (ndarray): The input image. It accepts:
|
| 216 |
+
1. np.uint8 type with range [0, 255];
|
| 217 |
+
2. np.float32 type with range [0, 1].
|
| 218 |
+
y_only (bool): Whether to only return Y channel. Default: False.
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
ndarray: The converted YCbCr image. The output image has the same type
|
| 222 |
+
and range as input image.
|
| 223 |
+
"""
|
| 224 |
+
img_type = img.dtype
|
| 225 |
+
img = _convert_input_type_range(img)
|
| 226 |
+
if y_only:
|
| 227 |
+
out_img = np.dot(img, [24.966, 128.553, 65.481]) + 16.0
|
| 228 |
+
else:
|
| 229 |
+
out_img = np.matmul(
|
| 230 |
+
img, [[24.966, 112.0, -18.214], [128.553, -74.203, -93.786], [65.481, -37.797, 112.0]]) + [16, 128, 128]
|
| 231 |
+
out_img = _convert_output_type_range(out_img, img_type)
|
| 232 |
+
return out_img
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def ycbcr2rgb(img):
|
| 236 |
+
"""Convert a YCbCr image to RGB image.
|
| 237 |
+
|
| 238 |
+
This function produces the same results as Matlab's ycbcr2rgb function.
|
| 239 |
+
It implements the ITU-R BT.601 conversion for standard-definition
|
| 240 |
+
television. See more details in
|
| 241 |
+
https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion.
|
| 242 |
+
|
| 243 |
+
It differs from a similar function in cv2.cvtColor: `YCrCb <-> RGB`.
|
| 244 |
+
In OpenCV, it implements a JPEG conversion. See more details in
|
| 245 |
+
https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion.
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
img (ndarray): The input image. It accepts:
|
| 249 |
+
1. np.uint8 type with range [0, 255];
|
| 250 |
+
2. np.float32 type with range [0, 1].
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
ndarray: The converted RGB image. The output image has the same type
|
| 254 |
+
and range as input image.
|
| 255 |
+
"""
|
| 256 |
+
img_type = img.dtype
|
| 257 |
+
img = _convert_input_type_range(img) * 255
|
| 258 |
+
out_img = np.matmul(img, [[0.00456621, 0.00456621, 0.00456621], [0, -0.00153632, 0.00791071],
|
| 259 |
+
[0.00625893, -0.00318811, 0]]) * 255.0 + [-222.921, 135.576, -276.836] # noqa: E126
|
| 260 |
+
out_img = _convert_output_type_range(out_img, img_type)
|
| 261 |
+
return out_img
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def ycbcr2bgr(img):
|
| 265 |
+
"""Convert a YCbCr image to BGR image.
|
| 266 |
+
|
| 267 |
+
The bgr version of ycbcr2rgb.
|
| 268 |
+
It implements the ITU-R BT.601 conversion for standard-definition
|
| 269 |
+
television. See more details in
|
| 270 |
+
https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion.
|
| 271 |
+
|
| 272 |
+
It differs from a similar function in cv2.cvtColor: `YCrCb <-> BGR`.
|
| 273 |
+
In OpenCV, it implements a JPEG conversion. See more details in
|
| 274 |
+
https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion.
|
| 275 |
+
|
| 276 |
+
Args:
|
| 277 |
+
img (ndarray): The input image. It accepts:
|
| 278 |
+
1. np.uint8 type with range [0, 255];
|
| 279 |
+
2. np.float32 type with range [0, 1].
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
ndarray: The converted BGR image. The output image has the same type
|
| 283 |
+
and range as input image.
|
| 284 |
+
"""
|
| 285 |
+
img_type = img.dtype
|
| 286 |
+
img = _convert_input_type_range(img) * 255
|
| 287 |
+
out_img = np.matmul(img, [[0.00456621, 0.00456621, 0.00456621], [0.00791071, -0.00153632, 0],
|
| 288 |
+
[0, -0.00318811, 0.00625893]]) * 255.0 + [-276.836, 135.576, -222.921] # noqa: E126
|
| 289 |
+
out_img = _convert_output_type_range(out_img, img_type)
|
| 290 |
+
return out_img
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def _convert_input_type_range(img):
|
| 294 |
+
"""Convert the type and range of the input image.
|
| 295 |
+
|
| 296 |
+
It converts the input image to np.float32 type and range of [0, 1].
|
| 297 |
+
It is mainly used for pre-processing the input image in colorspace
|
| 298 |
+
convertion functions such as rgb2ycbcr and ycbcr2rgb.
|
| 299 |
+
|
| 300 |
+
Args:
|
| 301 |
+
img (ndarray): The input image. It accepts:
|
| 302 |
+
1. np.uint8 type with range [0, 255];
|
| 303 |
+
2. np.float32 type with range [0, 1].
|
| 304 |
+
|
| 305 |
+
Returns:
|
| 306 |
+
(ndarray): The converted image with type of np.float32 and range of
|
| 307 |
+
[0, 1].
|
| 308 |
+
"""
|
| 309 |
+
img_type = img.dtype
|
| 310 |
+
img = img.astype(np.float32)
|
| 311 |
+
if img_type == np.float32:
|
| 312 |
+
pass
|
| 313 |
+
elif img_type == np.uint8:
|
| 314 |
+
img /= 255.
|
| 315 |
+
else:
|
| 316 |
+
raise TypeError('The img type should be np.float32 or np.uint8, ' f'but got {img_type}')
|
| 317 |
+
return img
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def _convert_output_type_range(img, dst_type):
|
| 321 |
+
"""Convert the type and range of the image according to dst_type.
|
| 322 |
+
|
| 323 |
+
It converts the image to desired type and range. If `dst_type` is np.uint8,
|
| 324 |
+
images will be converted to np.uint8 type with range [0, 255]. If
|
| 325 |
+
`dst_type` is np.float32, it converts the image to np.float32 type with
|
| 326 |
+
range [0, 1].
|
| 327 |
+
It is mainly used for post-processing images in colorspace convertion
|
| 328 |
+
functions such as rgb2ycbcr and ycbcr2rgb.
|
| 329 |
+
|
| 330 |
+
Args:
|
| 331 |
+
img (ndarray): The image to be converted with np.float32 type and
|
| 332 |
+
range [0, 255].
|
| 333 |
+
dst_type (np.uint8 | np.float32): If dst_type is np.uint8, it
|
| 334 |
+
converts the image to np.uint8 type with range [0, 255]. If
|
| 335 |
+
dst_type is np.float32, it converts the image to np.float32 type
|
| 336 |
+
with range [0, 1].
|
| 337 |
+
|
| 338 |
+
Returns:
|
| 339 |
+
(ndarray): The converted image with desired type and range.
|
| 340 |
+
"""
|
| 341 |
+
if dst_type not in (np.uint8, np.float32):
|
| 342 |
+
raise TypeError('The dst_type should be np.float32 or np.uint8, ' f'but got {dst_type}')
|
| 343 |
+
if dst_type == np.uint8:
|
| 344 |
+
img = img.round()
|
| 345 |
+
else:
|
| 346 |
+
img /= 255.
|
| 347 |
+
return img.astype(dst_type)
|
basicsr/utils/misc.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import random
|
| 4 |
+
import time
|
| 5 |
+
import torch
|
| 6 |
+
import numpy as np
|
| 7 |
+
from os import path as osp
|
| 8 |
+
|
| 9 |
+
from .dist_util import master_only
|
| 10 |
+
from .logger import get_root_logger
|
| 11 |
+
|
| 12 |
+
IS_HIGH_VERSION = [int(m) for m in list(re.findall(r"^([0-9]+)\.([0-9]+)\.([0-9]+)([^0-9][a-zA-Z0-9]*)?(\+git.*)?$",\
|
| 13 |
+
torch.__version__)[0][:3])] >= [1, 12, 0]
|
| 14 |
+
|
| 15 |
+
def gpu_is_available():
|
| 16 |
+
if IS_HIGH_VERSION:
|
| 17 |
+
if torch.backends.mps.is_available():
|
| 18 |
+
return True
|
| 19 |
+
return True if torch.cuda.is_available() and torch.backends.cudnn.is_available() else False
|
| 20 |
+
|
| 21 |
+
def get_device(gpu_id=None):
|
| 22 |
+
if gpu_id is None:
|
| 23 |
+
gpu_str = ''
|
| 24 |
+
elif isinstance(gpu_id, int):
|
| 25 |
+
gpu_str = f':{gpu_id}'
|
| 26 |
+
else:
|
| 27 |
+
raise TypeError('Input should be int value.')
|
| 28 |
+
|
| 29 |
+
if IS_HIGH_VERSION:
|
| 30 |
+
if torch.backends.mps.is_available():
|
| 31 |
+
return torch.device('mps'+gpu_str)
|
| 32 |
+
return torch.device('cuda'+gpu_str if torch.cuda.is_available() and torch.backends.cudnn.is_available() else 'cpu')
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def set_random_seed(seed):
|
| 36 |
+
"""Set random seeds."""
|
| 37 |
+
random.seed(seed)
|
| 38 |
+
np.random.seed(seed)
|
| 39 |
+
torch.manual_seed(seed)
|
| 40 |
+
torch.cuda.manual_seed(seed)
|
| 41 |
+
torch.cuda.manual_seed_all(seed)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_time_str():
|
| 45 |
+
return time.strftime('%Y%m%d_%H%M%S', time.localtime())
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def mkdir_and_rename(path):
|
| 49 |
+
"""mkdirs. If path exists, rename it with timestamp and create a new one.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
path (str): Folder path.
|
| 53 |
+
"""
|
| 54 |
+
if osp.exists(path):
|
| 55 |
+
new_name = path + '_archived_' + get_time_str()
|
| 56 |
+
print(f'Path already exists. Rename it to {new_name}', flush=True)
|
| 57 |
+
os.rename(path, new_name)
|
| 58 |
+
os.makedirs(path, exist_ok=True)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@master_only
|
| 62 |
+
def make_exp_dirs(opt):
|
| 63 |
+
"""Make dirs for experiments."""
|
| 64 |
+
path_opt = opt['path'].copy()
|
| 65 |
+
if opt['is_train']:
|
| 66 |
+
mkdir_and_rename(path_opt.pop('experiments_root'))
|
| 67 |
+
else:
|
| 68 |
+
mkdir_and_rename(path_opt.pop('results_root'))
|
| 69 |
+
for key, path in path_opt.items():
|
| 70 |
+
if ('strict_load' not in key) and ('pretrain_network' not in key) and ('resume' not in key):
|
| 71 |
+
os.makedirs(path, exist_ok=True)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def scandir(dir_path, suffix=None, recursive=False, full_path=False):
|
| 75 |
+
"""Scan a directory to find the interested files.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
dir_path (str): Path of the directory.
|
| 79 |
+
suffix (str | tuple(str), optional): File suffix that we are
|
| 80 |
+
interested in. Default: None.
|
| 81 |
+
recursive (bool, optional): If set to True, recursively scan the
|
| 82 |
+
directory. Default: False.
|
| 83 |
+
full_path (bool, optional): If set to True, include the dir_path.
|
| 84 |
+
Default: False.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
A generator for all the interested files with relative pathes.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
if (suffix is not None) and not isinstance(suffix, (str, tuple)):
|
| 91 |
+
raise TypeError('"suffix" must be a string or tuple of strings')
|
| 92 |
+
|
| 93 |
+
root = dir_path
|
| 94 |
+
|
| 95 |
+
def _scandir(dir_path, suffix, recursive):
|
| 96 |
+
for entry in os.scandir(dir_path):
|
| 97 |
+
if not entry.name.startswith('.') and entry.is_file():
|
| 98 |
+
if full_path:
|
| 99 |
+
return_path = entry.path
|
| 100 |
+
else:
|
| 101 |
+
return_path = osp.relpath(entry.path, root)
|
| 102 |
+
|
| 103 |
+
if suffix is None:
|
| 104 |
+
yield return_path
|
| 105 |
+
elif return_path.endswith(suffix):
|
| 106 |
+
yield return_path
|
| 107 |
+
else:
|
| 108 |
+
if recursive:
|
| 109 |
+
yield from _scandir(entry.path, suffix=suffix, recursive=recursive)
|
| 110 |
+
else:
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
return _scandir(dir_path, suffix=suffix, recursive=recursive)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def check_resume(opt, resume_iter):
|
| 117 |
+
"""Check resume states and pretrain_network paths.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
opt (dict): Options.
|
| 121 |
+
resume_iter (int): Resume iteration.
|
| 122 |
+
"""
|
| 123 |
+
logger = get_root_logger()
|
| 124 |
+
if opt['path']['resume_state']:
|
| 125 |
+
# get all the networks
|
| 126 |
+
networks = [key for key in opt.keys() if key.startswith('network_')]
|
| 127 |
+
flag_pretrain = False
|
| 128 |
+
for network in networks:
|
| 129 |
+
if opt['path'].get(f'pretrain_{network}') is not None:
|
| 130 |
+
flag_pretrain = True
|
| 131 |
+
if flag_pretrain:
|
| 132 |
+
logger.warning('pretrain_network path will be ignored during resuming.')
|
| 133 |
+
# set pretrained model paths
|
| 134 |
+
for network in networks:
|
| 135 |
+
name = f'pretrain_{network}'
|
| 136 |
+
basename = network.replace('network_', '')
|
| 137 |
+
if opt['path'].get('ignore_resume_networks') is None or (basename
|
| 138 |
+
not in opt['path']['ignore_resume_networks']):
|
| 139 |
+
opt['path'][name] = osp.join(opt['path']['models'], f'net_{basename}_{resume_iter}.pth')
|
| 140 |
+
logger.info(f"Set {name} to {opt['path'][name]}")
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def sizeof_fmt(size, suffix='B'):
|
| 144 |
+
"""Get human readable file size.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
size (int): File size.
|
| 148 |
+
suffix (str): Suffix. Default: 'B'.
|
| 149 |
+
|
| 150 |
+
Return:
|
| 151 |
+
str: Formated file siz.
|
| 152 |
+
"""
|
| 153 |
+
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
|
| 154 |
+
if abs(size) < 1024.0:
|
| 155 |
+
return f'{size:3.1f} {unit}{suffix}'
|
| 156 |
+
size /= 1024.0
|
| 157 |
+
return f'{size:3.1f} Y{suffix}'
|
basicsr/utils/options.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
import time
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
from os import path as osp
|
| 5 |
+
from basicsr.utils.misc import get_time_str
|
| 6 |
+
|
| 7 |
+
def ordered_yaml():
|
| 8 |
+
"""Support OrderedDict for yaml.
|
| 9 |
+
|
| 10 |
+
Returns:
|
| 11 |
+
yaml Loader and Dumper.
|
| 12 |
+
"""
|
| 13 |
+
try:
|
| 14 |
+
from yaml import CDumper as Dumper
|
| 15 |
+
from yaml import CLoader as Loader
|
| 16 |
+
except ImportError:
|
| 17 |
+
from yaml import Dumper, Loader
|
| 18 |
+
|
| 19 |
+
_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
|
| 20 |
+
|
| 21 |
+
def dict_representer(dumper, data):
|
| 22 |
+
return dumper.represent_dict(data.items())
|
| 23 |
+
|
| 24 |
+
def dict_constructor(loader, node):
|
| 25 |
+
return OrderedDict(loader.construct_pairs(node))
|
| 26 |
+
|
| 27 |
+
Dumper.add_representer(OrderedDict, dict_representer)
|
| 28 |
+
Loader.add_constructor(_mapping_tag, dict_constructor)
|
| 29 |
+
return Loader, Dumper
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def parse(opt_path, root_path, is_train=True):
|
| 33 |
+
"""Parse option file.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
opt_path (str): Option file path.
|
| 37 |
+
is_train (str): Indicate whether in training or not. Default: True.
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
(dict): Options.
|
| 41 |
+
"""
|
| 42 |
+
with open(opt_path, mode='r') as f:
|
| 43 |
+
Loader, _ = ordered_yaml()
|
| 44 |
+
opt = yaml.load(f, Loader=Loader)
|
| 45 |
+
|
| 46 |
+
opt['is_train'] = is_train
|
| 47 |
+
|
| 48 |
+
# opt['name'] = f"{get_time_str()}_{opt['name']}"
|
| 49 |
+
if opt['path'].get('resume_state', None): # Shangchen added
|
| 50 |
+
resume_state_path = opt['path'].get('resume_state')
|
| 51 |
+
opt['name'] = resume_state_path.split("/")[-3]
|
| 52 |
+
else:
|
| 53 |
+
opt['name'] = f"{get_time_str()}_{opt['name']}"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# datasets
|
| 57 |
+
for phase, dataset in opt['datasets'].items():
|
| 58 |
+
# for several datasets, e.g., test_1, test_2
|
| 59 |
+
phase = phase.split('_')[0]
|
| 60 |
+
dataset['phase'] = phase
|
| 61 |
+
if 'scale' in opt:
|
| 62 |
+
dataset['scale'] = opt['scale']
|
| 63 |
+
if dataset.get('dataroot_gt') is not None:
|
| 64 |
+
dataset['dataroot_gt'] = osp.expanduser(dataset['dataroot_gt'])
|
| 65 |
+
if dataset.get('dataroot_lq') is not None:
|
| 66 |
+
dataset['dataroot_lq'] = osp.expanduser(dataset['dataroot_lq'])
|
| 67 |
+
|
| 68 |
+
# paths
|
| 69 |
+
for key, val in opt['path'].items():
|
| 70 |
+
if (val is not None) and ('resume_state' in key or 'pretrain_network' in key):
|
| 71 |
+
opt['path'][key] = osp.expanduser(val)
|
| 72 |
+
|
| 73 |
+
if is_train:
|
| 74 |
+
experiments_root = osp.join(root_path, 'experiments', opt['name'])
|
| 75 |
+
opt['path']['experiments_root'] = experiments_root
|
| 76 |
+
opt['path']['models'] = osp.join(experiments_root, 'models')
|
| 77 |
+
opt['path']['training_states'] = osp.join(experiments_root, 'training_states')
|
| 78 |
+
opt['path']['log'] = experiments_root
|
| 79 |
+
opt['path']['visualization'] = osp.join(experiments_root, 'visualization')
|
| 80 |
+
|
| 81 |
+
else: # test
|
| 82 |
+
results_root = osp.join(root_path, 'results', opt['name'])
|
| 83 |
+
opt['path']['results_root'] = results_root
|
| 84 |
+
opt['path']['log'] = results_root
|
| 85 |
+
opt['path']['visualization'] = osp.join(results_root, 'visualization')
|
| 86 |
+
|
| 87 |
+
return opt
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def dict2str(opt, indent_level=1):
|
| 91 |
+
"""dict to string for printing options.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
opt (dict): Option dict.
|
| 95 |
+
indent_level (int): Indent level. Default: 1.
|
| 96 |
+
|
| 97 |
+
Return:
|
| 98 |
+
(str): Option string for printing.
|
| 99 |
+
"""
|
| 100 |
+
msg = '\n'
|
| 101 |
+
for k, v in opt.items():
|
| 102 |
+
if isinstance(v, dict):
|
| 103 |
+
msg += ' ' * (indent_level * 2) + k + ':['
|
| 104 |
+
msg += dict2str(v, indent_level + 1)
|
| 105 |
+
msg += ' ' * (indent_level * 2) + ']\n'
|
| 106 |
+
else:
|
| 107 |
+
msg += ' ' * (indent_level * 2) + k + ': ' + str(v) + '\n'
|
| 108 |
+
return msg
|
basicsr/utils/realesrgan_utils.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import math
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
import queue
|
| 6 |
+
import threading
|
| 7 |
+
import torch
|
| 8 |
+
from torch.nn import functional as F
|
| 9 |
+
from basicsr.utils.download_util import load_file_from_url
|
| 10 |
+
from basicsr.utils.misc import get_device
|
| 11 |
+
|
| 12 |
+
# ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 13 |
+
|
| 14 |
+
class RealESRGANer():
|
| 15 |
+
"""A helper class for upsampling images with RealESRGAN.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
scale (int): Upsampling scale factor used in the networks. It is usually 2 or 4.
|
| 19 |
+
model_path (str): The path to the pretrained model. It can be urls (will first download it automatically).
|
| 20 |
+
model (nn.Module): The defined network. Default: None.
|
| 21 |
+
tile (int): As too large images result in the out of GPU memory issue, so this tile option will first crop
|
| 22 |
+
input images into tiles, and then process each of them. Finally, they will be merged into one image.
|
| 23 |
+
0 denotes for do not use tile. Default: 0.
|
| 24 |
+
tile_pad (int): The pad size for each tile, to remove border artifacts. Default: 10.
|
| 25 |
+
pre_pad (int): Pad the input images to avoid border artifacts. Default: 10.
|
| 26 |
+
half (float): Whether to use half precision during inference. Default: False.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self,
|
| 30 |
+
scale,
|
| 31 |
+
model_path,
|
| 32 |
+
model=None,
|
| 33 |
+
tile=0,
|
| 34 |
+
tile_pad=10,
|
| 35 |
+
pre_pad=10,
|
| 36 |
+
half=False,
|
| 37 |
+
device=None,
|
| 38 |
+
gpu_id=None):
|
| 39 |
+
self.scale = scale
|
| 40 |
+
self.tile_size = tile
|
| 41 |
+
self.tile_pad = tile_pad
|
| 42 |
+
self.pre_pad = pre_pad
|
| 43 |
+
self.mod_scale = None
|
| 44 |
+
self.half = half
|
| 45 |
+
|
| 46 |
+
# initialize model
|
| 47 |
+
# if gpu_id:
|
| 48 |
+
# self.device = torch.device(
|
| 49 |
+
# f'cuda:{gpu_id}' if torch.cuda.is_available() else 'cpu') if device is None else device
|
| 50 |
+
# else:
|
| 51 |
+
# self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') if device is None else device
|
| 52 |
+
|
| 53 |
+
self.device = get_device(gpu_id) if device is None else device
|
| 54 |
+
|
| 55 |
+
# if the model_path starts with https, it will first download models to the folder: realesrgan/weights
|
| 56 |
+
if model_path.startswith('https://'):
|
| 57 |
+
model_path = load_file_from_url(
|
| 58 |
+
url=model_path, model_dir=os.path.join('weights/realesrgan'), progress=True, file_name=None)
|
| 59 |
+
loadnet = torch.load(model_path, map_location=torch.device('cpu'))
|
| 60 |
+
# prefer to use params_ema
|
| 61 |
+
if 'params_ema' in loadnet:
|
| 62 |
+
keyname = 'params_ema'
|
| 63 |
+
else:
|
| 64 |
+
keyname = 'params'
|
| 65 |
+
model.load_state_dict(loadnet[keyname], strict=True)
|
| 66 |
+
model.eval()
|
| 67 |
+
self.model = model.to(self.device)
|
| 68 |
+
if self.half:
|
| 69 |
+
self.model = self.model.half()
|
| 70 |
+
|
| 71 |
+
def pre_process(self, img):
|
| 72 |
+
"""Pre-process, such as pre-pad and mod pad, so that the images can be divisible
|
| 73 |
+
"""
|
| 74 |
+
img = torch.from_numpy(np.transpose(img, (2, 0, 1))).float()
|
| 75 |
+
self.img = img.unsqueeze(0).to(self.device)
|
| 76 |
+
if self.half:
|
| 77 |
+
self.img = self.img.half()
|
| 78 |
+
|
| 79 |
+
# pre_pad
|
| 80 |
+
if self.pre_pad != 0:
|
| 81 |
+
self.img = F.pad(self.img, (0, self.pre_pad, 0, self.pre_pad), 'reflect')
|
| 82 |
+
# mod pad for divisible borders
|
| 83 |
+
if self.scale == 2:
|
| 84 |
+
self.mod_scale = 2
|
| 85 |
+
elif self.scale == 1:
|
| 86 |
+
self.mod_scale = 4
|
| 87 |
+
if self.mod_scale is not None:
|
| 88 |
+
self.mod_pad_h, self.mod_pad_w = 0, 0
|
| 89 |
+
_, _, h, w = self.img.size()
|
| 90 |
+
if (h % self.mod_scale != 0):
|
| 91 |
+
self.mod_pad_h = (self.mod_scale - h % self.mod_scale)
|
| 92 |
+
if (w % self.mod_scale != 0):
|
| 93 |
+
self.mod_pad_w = (self.mod_scale - w % self.mod_scale)
|
| 94 |
+
self.img = F.pad(self.img, (0, self.mod_pad_w, 0, self.mod_pad_h), 'reflect')
|
| 95 |
+
|
| 96 |
+
def process(self):
|
| 97 |
+
# model inference
|
| 98 |
+
self.output = self.model(self.img)
|
| 99 |
+
|
| 100 |
+
def tile_process(self):
|
| 101 |
+
"""It will first crop input images to tiles, and then process each tile.
|
| 102 |
+
Finally, all the processed tiles are merged into one images.
|
| 103 |
+
|
| 104 |
+
Modified from: https://github.com/ata4/esrgan-launcher
|
| 105 |
+
"""
|
| 106 |
+
batch, channel, height, width = self.img.shape
|
| 107 |
+
output_height = height * self.scale
|
| 108 |
+
output_width = width * self.scale
|
| 109 |
+
output_shape = (batch, channel, output_height, output_width)
|
| 110 |
+
|
| 111 |
+
# start with black image
|
| 112 |
+
self.output = self.img.new_zeros(output_shape)
|
| 113 |
+
tiles_x = math.ceil(width / self.tile_size)
|
| 114 |
+
tiles_y = math.ceil(height / self.tile_size)
|
| 115 |
+
|
| 116 |
+
# loop over all tiles
|
| 117 |
+
for y in range(tiles_y):
|
| 118 |
+
for x in range(tiles_x):
|
| 119 |
+
# extract tile from input image
|
| 120 |
+
ofs_x = x * self.tile_size
|
| 121 |
+
ofs_y = y * self.tile_size
|
| 122 |
+
# input tile area on total image
|
| 123 |
+
input_start_x = ofs_x
|
| 124 |
+
input_end_x = min(ofs_x + self.tile_size, width)
|
| 125 |
+
input_start_y = ofs_y
|
| 126 |
+
input_end_y = min(ofs_y + self.tile_size, height)
|
| 127 |
+
|
| 128 |
+
# input tile area on total image with padding
|
| 129 |
+
input_start_x_pad = max(input_start_x - self.tile_pad, 0)
|
| 130 |
+
input_end_x_pad = min(input_end_x + self.tile_pad, width)
|
| 131 |
+
input_start_y_pad = max(input_start_y - self.tile_pad, 0)
|
| 132 |
+
input_end_y_pad = min(input_end_y + self.tile_pad, height)
|
| 133 |
+
|
| 134 |
+
# input tile dimensions
|
| 135 |
+
input_tile_width = input_end_x - input_start_x
|
| 136 |
+
input_tile_height = input_end_y - input_start_y
|
| 137 |
+
tile_idx = y * tiles_x + x + 1
|
| 138 |
+
input_tile = self.img[:, :, input_start_y_pad:input_end_y_pad, input_start_x_pad:input_end_x_pad]
|
| 139 |
+
|
| 140 |
+
# upscale tile
|
| 141 |
+
try:
|
| 142 |
+
with torch.no_grad():
|
| 143 |
+
output_tile = self.model(input_tile)
|
| 144 |
+
except RuntimeError as error:
|
| 145 |
+
print('Error', error)
|
| 146 |
+
# print(f'\tTile {tile_idx}/{tiles_x * tiles_y}')
|
| 147 |
+
|
| 148 |
+
# output tile area on total image
|
| 149 |
+
output_start_x = input_start_x * self.scale
|
| 150 |
+
output_end_x = input_end_x * self.scale
|
| 151 |
+
output_start_y = input_start_y * self.scale
|
| 152 |
+
output_end_y = input_end_y * self.scale
|
| 153 |
+
|
| 154 |
+
# output tile area without padding
|
| 155 |
+
output_start_x_tile = (input_start_x - input_start_x_pad) * self.scale
|
| 156 |
+
output_end_x_tile = output_start_x_tile + input_tile_width * self.scale
|
| 157 |
+
output_start_y_tile = (input_start_y - input_start_y_pad) * self.scale
|
| 158 |
+
output_end_y_tile = output_start_y_tile + input_tile_height * self.scale
|
| 159 |
+
|
| 160 |
+
# put tile into output image
|
| 161 |
+
self.output[:, :, output_start_y:output_end_y,
|
| 162 |
+
output_start_x:output_end_x] = output_tile[:, :, output_start_y_tile:output_end_y_tile,
|
| 163 |
+
output_start_x_tile:output_end_x_tile]
|
| 164 |
+
|
| 165 |
+
def post_process(self):
|
| 166 |
+
# remove extra pad
|
| 167 |
+
if self.mod_scale is not None:
|
| 168 |
+
_, _, h, w = self.output.size()
|
| 169 |
+
self.output = self.output[:, :, 0:h - self.mod_pad_h * self.scale, 0:w - self.mod_pad_w * self.scale]
|
| 170 |
+
# remove prepad
|
| 171 |
+
if self.pre_pad != 0:
|
| 172 |
+
_, _, h, w = self.output.size()
|
| 173 |
+
self.output = self.output[:, :, 0:h - self.pre_pad * self.scale, 0:w - self.pre_pad * self.scale]
|
| 174 |
+
return self.output
|
| 175 |
+
|
| 176 |
+
@torch.no_grad()
|
| 177 |
+
def enhance(self, img, outscale=None, alpha_upsampler='realesrgan'):
|
| 178 |
+
h_input, w_input = img.shape[0:2]
|
| 179 |
+
# img: numpy
|
| 180 |
+
img = img.astype(np.float32)
|
| 181 |
+
if np.max(img) > 256: # 16-bit image
|
| 182 |
+
max_range = 65535
|
| 183 |
+
print('\tInput is a 16-bit image')
|
| 184 |
+
else:
|
| 185 |
+
max_range = 255
|
| 186 |
+
img = img / max_range
|
| 187 |
+
if len(img.shape) == 2: # gray image
|
| 188 |
+
img_mode = 'L'
|
| 189 |
+
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
|
| 190 |
+
elif img.shape[2] == 4: # RGBA image with alpha channel
|
| 191 |
+
img_mode = 'RGBA'
|
| 192 |
+
alpha = img[:, :, 3]
|
| 193 |
+
img = img[:, :, 0:3]
|
| 194 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 195 |
+
if alpha_upsampler == 'realesrgan':
|
| 196 |
+
alpha = cv2.cvtColor(alpha, cv2.COLOR_GRAY2RGB)
|
| 197 |
+
else:
|
| 198 |
+
img_mode = 'RGB'
|
| 199 |
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 200 |
+
|
| 201 |
+
# ------------------- process image (without the alpha channel) ------------------- #
|
| 202 |
+
try:
|
| 203 |
+
with torch.no_grad():
|
| 204 |
+
self.pre_process(img)
|
| 205 |
+
if self.tile_size > 0:
|
| 206 |
+
self.tile_process()
|
| 207 |
+
else:
|
| 208 |
+
self.process()
|
| 209 |
+
output_img_t = self.post_process()
|
| 210 |
+
output_img = output_img_t.data.squeeze().float().cpu().clamp_(0, 1).numpy()
|
| 211 |
+
output_img = np.transpose(output_img[[2, 1, 0], :, :], (1, 2, 0))
|
| 212 |
+
if img_mode == 'L':
|
| 213 |
+
output_img = cv2.cvtColor(output_img, cv2.COLOR_BGR2GRAY)
|
| 214 |
+
del output_img_t
|
| 215 |
+
torch.cuda.empty_cache()
|
| 216 |
+
except RuntimeError as error:
|
| 217 |
+
print(f"Failed inference for RealESRGAN: {error}")
|
| 218 |
+
|
| 219 |
+
# ------------------- process the alpha channel if necessary ------------------- #
|
| 220 |
+
if img_mode == 'RGBA':
|
| 221 |
+
if alpha_upsampler == 'realesrgan':
|
| 222 |
+
self.pre_process(alpha)
|
| 223 |
+
if self.tile_size > 0:
|
| 224 |
+
self.tile_process()
|
| 225 |
+
else:
|
| 226 |
+
self.process()
|
| 227 |
+
output_alpha = self.post_process()
|
| 228 |
+
output_alpha = output_alpha.data.squeeze().float().cpu().clamp_(0, 1).numpy()
|
| 229 |
+
output_alpha = np.transpose(output_alpha[[2, 1, 0], :, :], (1, 2, 0))
|
| 230 |
+
output_alpha = cv2.cvtColor(output_alpha, cv2.COLOR_BGR2GRAY)
|
| 231 |
+
else: # use the cv2 resize for alpha channel
|
| 232 |
+
h, w = alpha.shape[0:2]
|
| 233 |
+
output_alpha = cv2.resize(alpha, (w * self.scale, h * self.scale), interpolation=cv2.INTER_LINEAR)
|
| 234 |
+
|
| 235 |
+
# merge the alpha channel
|
| 236 |
+
output_img = cv2.cvtColor(output_img, cv2.COLOR_BGR2BGRA)
|
| 237 |
+
output_img[:, :, 3] = output_alpha
|
| 238 |
+
|
| 239 |
+
# ------------------------------ return ------------------------------ #
|
| 240 |
+
if max_range == 65535: # 16-bit image
|
| 241 |
+
output = (output_img * 65535.0).round().astype(np.uint16)
|
| 242 |
+
else:
|
| 243 |
+
output = (output_img * 255.0).round().astype(np.uint8)
|
| 244 |
+
|
| 245 |
+
if outscale is not None and outscale != float(self.scale):
|
| 246 |
+
output = cv2.resize(
|
| 247 |
+
output, (
|
| 248 |
+
int(w_input * outscale),
|
| 249 |
+
int(h_input * outscale),
|
| 250 |
+
), interpolation=cv2.INTER_LANCZOS4)
|
| 251 |
+
|
| 252 |
+
return output, img_mode
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class PrefetchReader(threading.Thread):
|
| 256 |
+
"""Prefetch images.
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
img_list (list[str]): A image list of image paths to be read.
|
| 260 |
+
num_prefetch_queue (int): Number of prefetch queue.
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
def __init__(self, img_list, num_prefetch_queue):
|
| 264 |
+
super().__init__()
|
| 265 |
+
self.que = queue.Queue(num_prefetch_queue)
|
| 266 |
+
self.img_list = img_list
|
| 267 |
+
|
| 268 |
+
def run(self):
|
| 269 |
+
for img_path in self.img_list:
|
| 270 |
+
img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED)
|
| 271 |
+
self.que.put(img)
|
| 272 |
+
|
| 273 |
+
self.que.put(None)
|
| 274 |
+
|
| 275 |
+
def __next__(self):
|
| 276 |
+
next_item = self.que.get()
|
| 277 |
+
if next_item is None:
|
| 278 |
+
raise StopIteration
|
| 279 |
+
return next_item
|
| 280 |
+
|
| 281 |
+
def __iter__(self):
|
| 282 |
+
return self
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
class IOConsumer(threading.Thread):
|
| 286 |
+
|
| 287 |
+
def __init__(self, opt, que, qid):
|
| 288 |
+
super().__init__()
|
| 289 |
+
self._queue = que
|
| 290 |
+
self.qid = qid
|
| 291 |
+
self.opt = opt
|
| 292 |
+
|
| 293 |
+
def run(self):
|
| 294 |
+
while True:
|
| 295 |
+
msg = self._queue.get()
|
| 296 |
+
if isinstance(msg, str) and msg == 'quit':
|
| 297 |
+
break
|
| 298 |
+
|
| 299 |
+
output = msg['output']
|
| 300 |
+
save_path = msg['save_path']
|
| 301 |
+
cv2.imwrite(save_path, output)
|
| 302 |
+
print(f'IO worker {self.qid} is done.')
|
basicsr/utils/registry.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified from: https://github.com/facebookresearch/fvcore/blob/master/fvcore/common/registry.py # noqa: E501
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class Registry():
|
| 5 |
+
"""
|
| 6 |
+
The registry that provides name -> object mapping, to support third-party
|
| 7 |
+
users' custom modules.
|
| 8 |
+
|
| 9 |
+
To create a registry (e.g. a backbone registry):
|
| 10 |
+
|
| 11 |
+
.. code-block:: python
|
| 12 |
+
|
| 13 |
+
BACKBONE_REGISTRY = Registry('BACKBONE')
|
| 14 |
+
|
| 15 |
+
To register an object:
|
| 16 |
+
|
| 17 |
+
.. code-block:: python
|
| 18 |
+
|
| 19 |
+
@BACKBONE_REGISTRY.register()
|
| 20 |
+
class MyBackbone():
|
| 21 |
+
...
|
| 22 |
+
|
| 23 |
+
Or:
|
| 24 |
+
|
| 25 |
+
.. code-block:: python
|
| 26 |
+
|
| 27 |
+
BACKBONE_REGISTRY.register(MyBackbone)
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, name):
|
| 31 |
+
"""
|
| 32 |
+
Args:
|
| 33 |
+
name (str): the name of this registry
|
| 34 |
+
"""
|
| 35 |
+
self._name = name
|
| 36 |
+
self._obj_map = {}
|
| 37 |
+
|
| 38 |
+
def _do_register(self, name, obj):
|
| 39 |
+
assert (name not in self._obj_map), (f"An object named '{name}' was already registered "
|
| 40 |
+
f"in '{self._name}' registry!")
|
| 41 |
+
self._obj_map[name] = obj
|
| 42 |
+
|
| 43 |
+
def register(self, obj=None):
|
| 44 |
+
"""
|
| 45 |
+
Register the given object under the the name `obj.__name__`.
|
| 46 |
+
Can be used as either a decorator or not.
|
| 47 |
+
See docstring of this class for usage.
|
| 48 |
+
"""
|
| 49 |
+
if obj is None:
|
| 50 |
+
# used as a decorator
|
| 51 |
+
def deco(func_or_class):
|
| 52 |
+
name = func_or_class.__name__
|
| 53 |
+
self._do_register(name, func_or_class)
|
| 54 |
+
return func_or_class
|
| 55 |
+
|
| 56 |
+
return deco
|
| 57 |
+
|
| 58 |
+
# used as a function call
|
| 59 |
+
name = obj.__name__
|
| 60 |
+
self._do_register(name, obj)
|
| 61 |
+
|
| 62 |
+
def get(self, name):
|
| 63 |
+
ret = self._obj_map.get(name)
|
| 64 |
+
if ret is None:
|
| 65 |
+
raise KeyError(f"No object named '{name}' found in '{self._name}' registry!")
|
| 66 |
+
return ret
|
| 67 |
+
|
| 68 |
+
def __contains__(self, name):
|
| 69 |
+
return name in self._obj_map
|
| 70 |
+
|
| 71 |
+
def __iter__(self):
|
| 72 |
+
return iter(self._obj_map.items())
|
| 73 |
+
|
| 74 |
+
def keys(self):
|
| 75 |
+
return self._obj_map.keys()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
DATASET_REGISTRY = Registry('dataset')
|
| 79 |
+
ARCH_REGISTRY = Registry('arch')
|
| 80 |
+
MODEL_REGISTRY = Registry('model')
|
| 81 |
+
LOSS_REGISTRY = Registry('loss')
|
| 82 |
+
METRIC_REGISTRY = Registry('metric')
|
basicsr/utils/video_util.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
The code is modified from the Real-ESRGAN:
|
| 3 |
+
https://github.com/xinntao/Real-ESRGAN/blob/master/inference_realesrgan_video.py
|
| 4 |
+
|
| 5 |
+
'''
|
| 6 |
+
import cv2
|
| 7 |
+
import sys
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
import ffmpeg
|
| 12 |
+
except ImportError:
|
| 13 |
+
import pip
|
| 14 |
+
pip.main(['install', '--user', 'ffmpeg-python'])
|
| 15 |
+
import ffmpeg
|
| 16 |
+
|
| 17 |
+
def get_video_meta_info(video_path):
|
| 18 |
+
ret = {}
|
| 19 |
+
probe = ffmpeg.probe(video_path)
|
| 20 |
+
video_streams = [stream for stream in probe['streams'] if stream['codec_type'] == 'video']
|
| 21 |
+
has_audio = any(stream['codec_type'] == 'audio' for stream in probe['streams'])
|
| 22 |
+
ret['width'] = video_streams[0]['width']
|
| 23 |
+
ret['height'] = video_streams[0]['height']
|
| 24 |
+
ret['fps'] = eval(video_streams[0]['avg_frame_rate'])
|
| 25 |
+
ret['audio'] = ffmpeg.input(video_path).audio if has_audio else None
|
| 26 |
+
ret['nb_frames'] = int(video_streams[0]['nb_frames'])
|
| 27 |
+
return ret
|
| 28 |
+
|
| 29 |
+
class VideoReader:
|
| 30 |
+
def __init__(self, video_path):
|
| 31 |
+
self.paths = [] # for image&folder type
|
| 32 |
+
self.audio = None
|
| 33 |
+
try:
|
| 34 |
+
self.stream_reader = (
|
| 35 |
+
ffmpeg.input(video_path).output('pipe:', format='rawvideo', pix_fmt='bgr24',
|
| 36 |
+
loglevel='error').run_async(
|
| 37 |
+
pipe_stdin=True, pipe_stdout=True, cmd='ffmpeg'))
|
| 38 |
+
except FileNotFoundError:
|
| 39 |
+
print('Please install ffmpeg (not ffmpeg-python) by running\n',
|
| 40 |
+
'\t$ conda install -c conda-forge ffmpeg')
|
| 41 |
+
sys.exit(0)
|
| 42 |
+
|
| 43 |
+
meta = get_video_meta_info(video_path)
|
| 44 |
+
self.width = meta['width']
|
| 45 |
+
self.height = meta['height']
|
| 46 |
+
self.input_fps = meta['fps']
|
| 47 |
+
self.audio = meta['audio']
|
| 48 |
+
self.nb_frames = meta['nb_frames']
|
| 49 |
+
|
| 50 |
+
self.idx = 0
|
| 51 |
+
|
| 52 |
+
def get_resolution(self):
|
| 53 |
+
return self.height, self.width
|
| 54 |
+
|
| 55 |
+
def get_fps(self):
|
| 56 |
+
if self.input_fps is not None:
|
| 57 |
+
return self.input_fps
|
| 58 |
+
return 24
|
| 59 |
+
|
| 60 |
+
def get_audio(self):
|
| 61 |
+
return self.audio
|
| 62 |
+
|
| 63 |
+
def __len__(self):
|
| 64 |
+
return self.nb_frames
|
| 65 |
+
|
| 66 |
+
def get_frame_from_stream(self):
|
| 67 |
+
img_bytes = self.stream_reader.stdout.read(self.width * self.height * 3) # 3 bytes for one pixel
|
| 68 |
+
if not img_bytes:
|
| 69 |
+
return None
|
| 70 |
+
img = np.frombuffer(img_bytes, np.uint8).reshape([self.height, self.width, 3])
|
| 71 |
+
return img
|
| 72 |
+
|
| 73 |
+
def get_frame_from_list(self):
|
| 74 |
+
if self.idx >= self.nb_frames:
|
| 75 |
+
return None
|
| 76 |
+
img = cv2.imread(self.paths[self.idx])
|
| 77 |
+
self.idx += 1
|
| 78 |
+
return img
|
| 79 |
+
|
| 80 |
+
def get_frame(self):
|
| 81 |
+
return self.get_frame_from_stream()
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def close(self):
|
| 85 |
+
self.stream_reader.stdin.close()
|
| 86 |
+
self.stream_reader.wait()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class VideoWriter:
|
| 90 |
+
def __init__(self, video_save_path, height, width, fps, audio):
|
| 91 |
+
if height > 2160:
|
| 92 |
+
print('You are generating video that is larger than 4K, which will be very slow due to IO speed.',
|
| 93 |
+
'We highly recommend to decrease the outscale(aka, -s).')
|
| 94 |
+
if audio is not None:
|
| 95 |
+
self.stream_writer = (
|
| 96 |
+
ffmpeg.input('pipe:', format='rawvideo', pix_fmt='bgr24', s=f'{width}x{height}',
|
| 97 |
+
framerate=fps).output(
|
| 98 |
+
audio,
|
| 99 |
+
video_save_path,
|
| 100 |
+
pix_fmt='yuv420p',
|
| 101 |
+
vcodec='libx264',
|
| 102 |
+
loglevel='error',
|
| 103 |
+
acodec='copy').overwrite_output().run_async(
|
| 104 |
+
pipe_stdin=True, pipe_stdout=True, cmd='ffmpeg'))
|
| 105 |
+
else:
|
| 106 |
+
self.stream_writer = (
|
| 107 |
+
ffmpeg.input('pipe:', format='rawvideo', pix_fmt='bgr24', s=f'{width}x{height}',
|
| 108 |
+
framerate=fps).output(
|
| 109 |
+
video_save_path, pix_fmt='yuv420p', vcodec='libx264',
|
| 110 |
+
loglevel='error').overwrite_output().run_async(
|
| 111 |
+
pipe_stdin=True, pipe_stdout=True, cmd='ffmpeg'))
|
| 112 |
+
|
| 113 |
+
def write_frame(self, frame):
|
| 114 |
+
try:
|
| 115 |
+
frame = frame.astype(np.uint8).tobytes()
|
| 116 |
+
self.stream_writer.stdin.write(frame)
|
| 117 |
+
except BrokenPipeError:
|
| 118 |
+
print('Please re-install ffmpeg and libx264 by running\n',
|
| 119 |
+
'\t$ conda install -c conda-forge ffmpeg\n',
|
| 120 |
+
'\t$ conda install -c conda-forge x264')
|
| 121 |
+
sys.exit(0)
|
| 122 |
+
|
| 123 |
+
def close(self):
|
| 124 |
+
self.stream_writer.stdin.close()
|
| 125 |
+
self.stream_writer.wait()
|