Scrappy-Doo commited on
Commit
99c02ac
·
verified ·
1 Parent(s): a9212ad

Upload 13 files

Browse files
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()