%Fi88`XQmXnsZP>mKD}4V~
zuveuO4YIfTdGA-NHanOVBtgu5Up9O{PT2(hnfTK6{aceDs%GF&miA#-&vGI45dP~J
zkp4tMhqtJUSHZG08#_t;p#VF>?~qnyKLM`n<4CiLwS68%NAXsD#93UTA}^*Z=mFUq
z05uJ+12jrMp*sB-TK}dd{TBq@k7$j4Xw~UItp@#v<%^8oqDS?0-RtzJbG~!t
z`rMq)@cirIBVnFq>>t!PdTdm#p_G3BaK??4Wkz6RW?&lBHd8CG^xF<>{dNLJzumyo
z??%wT+e(|6ANYEWowl-e(AIS)oy+Ef`D`Ir&}}#EWQ)OKwiGO7%fYhl^U{@UHCQ#+
zO~xC%`H1nR@CR%AR&a`&ci5JH2-(<{sq7m`T&cz#aepN8Slrq&l+B|m!rOT-i~0g{
zPQ{6gM`bOgeU9`25dYN
z8~?=&Qn&ed^>fxUxW(;9?EvbUv{7wsDR>nCqDvj607_?sD&uy>A5b!r_g_25)lz
zksU02&TJNRpxTyNye*>iojbR0zJ2QkR9T8@@2ua&^K$p24{m&Lxf^lbjf;#_r2Ds4
zRr6Q@s!@!puuS%ZYHBrA4~M9ZN)hh%(qyPSs>-5LoxABs&eT3#(3)P1R-`&=3}taw
z`Fd`d^s`8{?ushvhq#-*m^Oy9Mzc)vIMfRkzrOh;)jqNNlNQasgl$L`y+4Ld958Fm
zPPnA}<0CGqgPk_KWFD{1?w#J`P8Le302Ql@n(dnt^MDzwvT9==?a;1G
zpi^5ww`K#E8~5DuYUOeC58N-=AI*K^H%5)Iuu*%!X59e#braaCePFw80q5#AaK4@c
zF4XhDPQ3tJtUJJ^dJ(u>F9BETW#DSP0$i(Y;HlaHu4@Zo)dqLCyKmlLTEflh^yCcp
zxOZSq&K@w#JI5ix;2dvW0V$A6-h$`2Q~yxz)Nz!Z7rEG5PV!HqG=b5;kGf!M`?K#6`}!JdPw(v*dicH
zkmmvT3Zh=QL-iMwQ;vp0$~Ic6akQ7DCCV8_)ov;0Y4!qj%)echLRLwUzawQKIH`Si}EZw%&tR-V_^ZB<6YjcvEiwE*WY#oz1&_f?fcl9HblDr%q
z0ju3$W3c?#dT_lmI6U_T`{b>${cy7~Csu797!#XY2gcap_SpK6ea!B!8(;(QfirQr
zb6^N)8@yYx9@|*r36^d#0?bCVA|@-#5ojehsw-Z=nD8RqI6mg{nQ7
ztFi0Yshz<*B1Q8v!~$AJrDxtV%fD3~*u~*KMj3B?X6b0rKFuZzReR9ETDMvx_8EXV
z2Fv>v+AEV))YmZPVb03r6tTi!{T}mx3si>`R2EfhEHmGrqM;M(ROul^6k2fJ8S23o3D+j=R39g^vzGU
zkrgb#60OP30fW81e!y_!w6=-0O-j%Px8
zgbrHVHJZ5q&E&A1VhKVe}AFcOzstZdB?_$J6Zi<|Wyn
z701M?KNsBJIsb+3=xiR?ucSEJ~s5xEMZ^-^YxtIeD4ZUjYQ(?){Kr
zk@Ct>9E-A4MyR}Dl7>mv4_rV9-5;fEl=%f7;&49-W980N-=Hg`A^9%tdz0Wea7&_Z
z)e6JDsFF%#VfZt2l)nZr%QtLe-Y|_0-bCCh#**oy2L)s2%yU-9YU{prbIs_OCg$0g
zZ|fDd(ZJ|B&A^K1%&=+3lF>HSuy5ORE!#McGknW6_Rc;lC>+bVWlRIYQ8Ghs3Jy9T
zkfBsWib(R{8pbU|5i1qBHbAV@8vdFRyfDO!F#Idl>a2qqdmGOZLC@M+q@*E_G7*N#
z55uhBBm55uRXYstkD_!of=zi5quVMgsjLM5Y-}V*sYJwL`o4ASoLY;Q7hzw*yy!cW
zJ2RVzX2#SrHw(S7NYkV6OCNy``XNoyp_uX+GJPG28%oWEhVLu%KGrV_ot4&
zsC&1$pwEjak9y*8sx=+d+%$Dv?eTPnfj=Fizc|y>%$xl$(sG`#V||(0m>XB7cX5?0
Vq9n)d7!CTFgl$+^2bSEn{sVd2*P8$U
literal 0
HcmV?d00001
diff --git a/dataset/batch_transform.py b/dataset/batch_transform.py
new file mode 100644
index 0000000..ef1a70e
--- /dev/null
+++ b/dataset/batch_transform.py
@@ -0,0 +1,374 @@
+from typing import Any, overload, Dict, Union, List, Sequence
+import random
+
+import torch
+from torch.nn import functional as F
+import numpy as np
+
+from utils.image import USMSharp, DiffJPEG, filter2D
+from utils.degradation import (
+ random_add_gaussian_noise_pt, random_add_poisson_noise_pt
+)
+
+
+class BatchTransform:
+
+ @overload
+ def __call__(self, batch: Any) -> Any:
+ ...
+
+
+class IdentityBatchTransform(BatchTransform):
+
+ def __call__(self, batch: Any) -> Any:
+ return batch
+
+
+class RealESRGANBatchTransform(BatchTransform):
+ """
+ It's too slow to process a batch of images under RealESRGAN degradation
+ model on CPU (by dataloader), which may cost 0.2 ~ 1 second per image.
+ So we execute the degradation process on GPU after loading a batch of images
+ and kernels from dataloader.
+ """
+ def __init__(
+ self,
+ use_sharpener: bool,
+ resize_hq: bool,
+ queue_size: int,
+ resize_prob: Sequence[float],
+ resize_range: Sequence[float],
+ gray_noise_prob: float,
+ gaussian_noise_prob: float,
+ noise_range: Sequence[float],
+ poisson_scale_range: Sequence[float],
+ jpeg_range: Sequence[int],
+ second_blur_prob: float,
+ stage2_scale: Union[float, Sequence[Union[float, int]]],
+ resize_prob2: Sequence[float],
+ resize_range2: Sequence[float],
+ gray_noise_prob2: float,
+ gaussian_noise_prob2: float,
+ noise_range2: Sequence[float],
+ poisson_scale_range2: Sequence[float],
+ jpeg_range2: Sequence[int]
+ ) -> "RealESRGANBatchTransform":
+ super().__init__()
+ # resize settings for the first degradation process
+ self.resize_prob = resize_prob
+ self.resize_range = resize_range
+
+ # noise settings for the first degradation process
+ self.gray_noise_prob = gray_noise_prob
+ self.gaussian_noise_prob = gaussian_noise_prob
+ self.noise_range = noise_range
+ self.poisson_scale_range = poisson_scale_range
+ self.jpeg_range = jpeg_range
+
+ self.second_blur_prob = second_blur_prob
+ self.stage2_scale = stage2_scale
+ assert (
+ isinstance(stage2_scale, (float, int)) or (
+ isinstance(stage2_scale, Sequence) and len(stage2_scale) == 2 and
+ all(isinstance(x, (float, int)) for x in stage2_scale)
+ )
+ ), f"stage2_scale can not be {type(stage2_scale)}"
+
+ # resize settings for the second degradation process
+ self.resize_prob2 = resize_prob2
+ self.resize_range2 = resize_range2
+
+ # noise settings for the second degradation process
+ self.gray_noise_prob2 = gray_noise_prob2
+ self.gaussian_noise_prob2 = gaussian_noise_prob2
+ self.noise_range2 = noise_range2
+ self.poisson_scale_range2 = poisson_scale_range2
+ self.jpeg_range2 = jpeg_range2
+
+ self.use_sharpener = use_sharpener
+ if self.use_sharpener:
+ self.usm_sharpener = USMSharp()
+ else:
+ self.usm_sharpener = None
+ self.resize_hq = resize_hq
+ self.queue_size = queue_size
+ self.jpeger = DiffJPEG(differentiable=False)
+
+ @torch.no_grad()
+ def _dequeue_and_enqueue(self):
+ """It is the training pair pool for increasing the diversity in a batch.
+
+ Batch processing limits the diversity of synthetic degradations in a batch. For example, samples in a
+ batch could not have different resize scaling factors. Therefore, we employ this training pair pool
+ to increase the degradation diversity in a batch.
+ """
+ # initialize
+ b, c, h, w = self.lq.size()
+ if not hasattr(self, "queue_lr"):
+ # TODO: Being multiple of batch_size seems not necessary for queue_size
+ assert self.queue_size % b == 0, f"queue size {self.queue_size} should be divisible by batch size {b}"
+ self.queue_lr = torch.zeros(self.queue_size, c, h, w).to(self.lq)
+ _, c, h, w = self.gt.size()
+ self.queue_gt = torch.zeros(self.queue_size, c, h, w).to(self.lq)
+ self.queue_ptr = 0
+ if self.queue_ptr == self.queue_size: # the pool is full
+ # do dequeue and enqueue
+ # shuffle
+ idx = torch.randperm(self.queue_size)
+ self.queue_lr = self.queue_lr[idx]
+ self.queue_gt = self.queue_gt[idx]
+ # get first b samples
+ lq_dequeue = self.queue_lr[0:b, :, :, :].clone()
+ gt_dequeue = self.queue_gt[0:b, :, :, :].clone()
+ # update the queue
+ self.queue_lr[0:b, :, :, :] = self.lq.clone()
+ self.queue_gt[0:b, :, :, :] = self.gt.clone()
+
+ self.lq = lq_dequeue
+ self.gt = gt_dequeue
+ else:
+ # only do enqueue
+ self.queue_lr[self.queue_ptr:self.queue_ptr + b, :, :, :] = self.lq.clone()
+ self.queue_gt[self.queue_ptr:self.queue_ptr + b, :, :, :] = self.gt.clone()
+ self.queue_ptr = self.queue_ptr + b
+
+ @torch.no_grad()
+ def __call__(self, batch: Dict[str, Union[torch.Tensor, str]]) -> Dict[str, Union[torch.Tensor, List[str]]]:
+ # training data synthesis
+ hq = batch["hq"]
+ if self.use_sharpener:
+ self.usm_sharpener.to(hq)
+ hq = self.usm_sharpener(hq)
+ self.jpeger.to(hq)
+
+ kernel1 = batch["kernel1"]
+ kernel2 = batch["kernel2"]
+ sinc_kernel = batch["sinc_kernel"]
+
+ ori_h, ori_w = hq.size()[2:4]
+
+ # ----------------------- The first degradation process ----------------------- #
+ # blur
+ out = filter2D(hq, kernel1)
+ # random resize
+ updown_type = random.choices(["up", "down", "keep"], self.resize_prob)[0]
+ if updown_type == "up":
+ scale = np.random.uniform(1, self.resize_range[1])
+ elif updown_type == "down":
+ scale = np.random.uniform(self.resize_range[0], 1)
+ else:
+ scale = 1
+ mode = random.choice(["area", "bilinear", "bicubic"])
+ out = F.interpolate(out, scale_factor=scale, mode=mode)
+ # add noise
+ if np.random.uniform() < self.gaussian_noise_prob:
+ out = random_add_gaussian_noise_pt(
+ out, sigma_range=self.noise_range, clip=True,
+ rounds=False, gray_prob=self.gray_noise_prob
+ )
+ else:
+ out = random_add_poisson_noise_pt(
+ out,
+ scale_range=self.poisson_scale_range,
+ gray_prob=self.gray_noise_prob,
+ clip=True,
+ rounds=False
+ )
+ # JPEG compression
+ jpeg_p = out.new_zeros(out.size(0)).uniform_(*self.jpeg_range)
+ # clamp to [0, 1], otherwise JPEGer will result in unpleasant artifacts
+ out = torch.clamp(out, 0, 1)
+ out = self.jpeger(out, quality=jpeg_p)
+
+ # ----------------------- The second degradation process ----------------------- #
+ # blur
+ if np.random.uniform() < self.second_blur_prob:
+ out = filter2D(out, kernel2)
+
+ # select scale of second degradation stage
+ if isinstance(self.stage2_scale, Sequence):
+ min_scale, max_scale = self.stage2_scale
+ stage2_scale = np.random.uniform(min_scale, max_scale)
+ else:
+ stage2_scale = self.stage2_scale
+ stage2_h, stage2_w = int(ori_h / stage2_scale), int(ori_w / stage2_scale)
+ # print(f"stage2 scale = {stage2_scale}")
+
+ # random resize
+ updown_type = random.choices(["up", "down", "keep"], self.resize_prob2)[0]
+ if updown_type == "up":
+ scale = np.random.uniform(1, self.resize_range2[1])
+ elif updown_type == "down":
+ scale = np.random.uniform(self.resize_range2[0], 1)
+ else:
+ scale = 1
+ mode = random.choice(["area", "bilinear", "bicubic"])
+ out = F.interpolate(
+ out, size=(int(stage2_h * scale), int(stage2_w * scale)), mode=mode
+ )
+ # add noise
+ if np.random.uniform() < self.gaussian_noise_prob2:
+ out = random_add_gaussian_noise_pt(
+ out, sigma_range=self.noise_range2, clip=True,
+ rounds=False, gray_prob=self.gray_noise_prob2
+ )
+ else:
+ out = random_add_poisson_noise_pt(
+ out,
+ scale_range=self.poisson_scale_range2,
+ gray_prob=self.gray_noise_prob2,
+ clip=True,
+ rounds=False
+ )
+
+ # JPEG compression + the final sinc filter
+ # We also need to resize images to desired sizes. We group [resize back + sinc filter] together
+ # as one operation.
+ # We consider two orders:
+ # 1. [resize back + sinc filter] + JPEG compression
+ # 2. JPEG compression + [resize back + sinc filter]
+ # Empirically, we find other combinations (sinc + JPEG + Resize) will introduce twisted lines.
+ if np.random.uniform() < 0.5:
+ # resize back + the final sinc filter
+ mode = random.choice(["area", "bilinear", "bicubic"])
+ out = F.interpolate(out, size=(stage2_h, stage2_w), mode=mode)
+ out = filter2D(out, sinc_kernel)
+ # JPEG compression
+ jpeg_p = out.new_zeros(out.size(0)).uniform_(*self.jpeg_range2)
+ out = torch.clamp(out, 0, 1)
+ out = self.jpeger(out, quality=jpeg_p)
+ else:
+ # JPEG compression
+ jpeg_p = out.new_zeros(out.size(0)).uniform_(*self.jpeg_range2)
+ out = torch.clamp(out, 0, 1)
+ out = self.jpeger(out, quality=jpeg_p)
+ # resize back + the final sinc filter
+ mode = random.choice(["area", "bilinear", "bicubic"])
+ out = F.interpolate(out, size=(stage2_h, stage2_w), mode=mode)
+ out = filter2D(out, sinc_kernel)
+
+ # resize back to gt_size since We are doing restoration task
+ if stage2_scale != 1:
+ out = F.interpolate(out, size=(ori_h, ori_w), mode="bicubic")
+ # clamp and round
+ lq = torch.clamp((out * 255.0).round(), 0, 255) / 255.
+
+ if self.resize_hq and stage2_scale != 1:
+ # resize hq
+ hq = F.interpolate(hq, size=(stage2_h, stage2_w), mode="bicubic", antialias=True)
+ hq = F.interpolate(hq, size=(ori_h, ori_w), mode="bicubic", antialias=True)
+
+ self.gt = hq
+ self.lq = lq
+ self._dequeue_and_enqueue()
+
+ # [0, 1], float32, rgb, nhwc
+ lq = self.lq.float().permute(0, 2, 3, 1).contiguous()
+ # [-1, 1], float32, rgb, nhwc
+ hq = (self.gt * 2 - 1).float().permute(0, 2, 3, 1).contiguous()
+
+ return dict(jpg=hq, hint=lq, txt=batch["txt"])
+
+
+class BicubicBatchTransform(BatchTransform):
+ """
+ It's too slow to process a batch of images under RealESRGAN degradation
+ model on CPU (by dataloader), which may cost 0.2 ~ 1 second per image.
+ So we execute the degradation process on GPU after loading a batch of images
+ and kernels from dataloader.
+ """
+ def __init__(
+ self,
+ use_sharpener: bool,
+ resize_hq: bool,
+ queue_size: int,
+ scale: Union[float, Sequence[Union[float, int]]],
+ ) -> "BicubicBatchTransform":
+ super().__init__()
+
+ self.scale = scale
+ assert (
+ isinstance(scale, (float, int)) or (
+ isinstance(scale, Sequence) and len(scale) == 2 and
+ all(isinstance(x, (float, int)) for x in scale)
+ )
+ ), f"scale can not be {type(scale)}"
+
+ self.use_sharpener = use_sharpener
+ if self.use_sharpener:
+ self.usm_sharpener = USMSharp()
+ else:
+ self.usm_sharpener = None
+ self.resize_hq = resize_hq
+ self.queue_size = queue_size
+
+
+ @torch.no_grad()
+ def _dequeue_and_enqueue(self):
+ """It is the training pair pool for increasing the diversity in a batch.
+
+ Batch processing limits the diversity of synthetic degradations in a batch. For example, samples in a
+ batch could not have different resize scaling factors. Therefore, we employ this training pair pool
+ to increase the degradation diversity in a batch.
+ """
+ # initialize
+ b, c, h, w = self.lq.size()
+ if not hasattr(self, "queue_lr"):
+ # TODO: Being multiple of batch_size seems not necessary for queue_size
+ assert self.queue_size % b == 0, f"queue size {self.queue_size} should be divisible by batch size {b}"
+ self.queue_lr = torch.zeros(self.queue_size, c, h, w).to(self.lq)
+ _, c, h, w = self.gt.size()
+ self.queue_gt = torch.zeros(self.queue_size, c, h, w).to(self.lq)
+ self.queue_ptr = 0
+ if self.queue_ptr == self.queue_size: # the pool is full
+ # do dequeue and enqueue
+ # shuffle
+ idx = torch.randperm(self.queue_size)
+ self.queue_lr = self.queue_lr[idx]
+ self.queue_gt = self.queue_gt[idx]
+ # get first b samples
+ lq_dequeue = self.queue_lr[0:b, :, :, :].clone()
+ gt_dequeue = self.queue_gt[0:b, :, :, :].clone()
+ # update the queue
+ self.queue_lr[0:b, :, :, :] = self.lq.clone()
+ self.queue_gt[0:b, :, :, :] = self.gt.clone()
+
+ self.lq = lq_dequeue
+ self.gt = gt_dequeue
+ else:
+ # only do enqueue
+ self.queue_lr[self.queue_ptr:self.queue_ptr + b, :, :, :] = self.lq.clone()
+ self.queue_gt[self.queue_ptr:self.queue_ptr + b, :, :, :] = self.gt.clone()
+ self.queue_ptr = self.queue_ptr + b
+
+ @torch.no_grad()
+ def __call__(self, batch: Dict[str, Union[torch.Tensor, str]]) -> Dict[str, Union[torch.Tensor, List[str]]]:
+ # training data synthesis
+ hq = batch["hq"]
+ if self.use_sharpener:
+ self.usm_sharpener.to(hq)
+ hq = self.usm_sharpener(hq)
+
+ ori_h, ori_w = hq.size()[2:4]
+ h, w = int(ori_h / self.scale), int(ori_w / self.scale)
+
+ # if self.resize_hq and self.scale != 1:
+ # # resize hq
+ # lq = F.interpolate(hq, size=(h, w), mode="bicubic", antialias=True)
+
+ out = F.interpolate(hq, size=(h, w), mode="bicubic", antialias=True)
+ out = F.interpolate(out, size=(ori_h, ori_w), mode="bicubic", antialias=True)
+
+ # clamp and round
+ lq = torch.clamp((out * 255.0).round(), 0, 255) / 255.
+
+ self.gt = hq
+ self.lq = lq
+ self._dequeue_and_enqueue()
+
+ # [0, 1], float32, rgb, nhwc
+ lq = self.lq.float().permute(0, 2, 3, 1).contiguous()
+ # [-1, 1], float32, rgb, nhwc
+ hq = (self.gt * 2 - 1).float().permute(0, 2, 3, 1).contiguous()
+
+ return dict(jpg=hq, hint=lq, txt=batch["txt"])
\ No newline at end of file
diff --git a/dataset/bicubic_torchvision.py b/dataset/bicubic_torchvision.py
new file mode 100644
index 0000000..c54f513
--- /dev/null
+++ b/dataset/bicubic_torchvision.py
@@ -0,0 +1,107 @@
+from typing import Dict, Sequence
+import math
+import random
+import time
+
+import numpy as np
+import torch
+from torch.utils import data
+from PIL import Image
+
+from utils.degradation import circular_lowpass_kernel, random_mixed_kernels
+from utils.image import augment, random_crop_arr, center_crop_arr
+from utils.file import load_file_list
+
+
+class BicubicDataset(data.Dataset):
+ """
+ # TODO: add comment
+ """
+
+ def __init__(
+ self,
+ file_list: str,
+ out_size: int,
+ crop_type: str,
+ use_hflip: bool,
+ use_rot: bool
+ ) -> "BicubicDataset":
+ super(BicubicDataset, self).__init__()
+ self.paths = load_file_list(file_list)
+ self.out_size = out_size
+ self.crop_type = crop_type
+ assert self.crop_type in ["center", "random", "none"], f"invalid crop type: {self.crop_type}"
+
+ # self.blur_kernel_size = blur_kernel_size
+ # self.kernel_list = kernel_list
+ # # a list for each kernel probability
+ # self.kernel_prob = kernel_prob
+ # self.blur_sigma = blur_sigma
+ # # betag used in generalized Gaussian blur kernels
+ # self.betag_range = betag_range
+ # # betap used in plateau blur kernels
+ # self.betap_range = betap_range
+ # # the probability for sinc filters
+ # self.sinc_prob = sinc_prob
+
+ # self.blur_kernel_size2 = blur_kernel_size2
+ # self.kernel_list2 = kernel_list2
+ # self.kernel_prob2 = kernel_prob2
+ # self.blur_sigma2 = blur_sigma2
+ # self.betag_range2 = betag_range2
+ # self.betap_range2 = betap_range2
+ # self.sinc_prob2 = sinc_prob2
+
+ # # a final sinc filter
+ # self.final_sinc_prob = final_sinc_prob
+
+ self.use_hflip = use_hflip
+ self.use_rot = use_rot
+
+ # kernel size ranges from 7 to 21
+ # self.kernel_range = [2 * v + 1 for v in range(3, 11)]
+ # # TODO: kernel range is now hard-coded, should be in the configure file
+ # # convolving with pulse tensor brings no blurry effect
+ # self.pulse_tensor = torch.zeros(21, 21).float()
+ # self.pulse_tensor[10, 10] = 1
+
+ @torch.no_grad()
+ def __getitem__(self, index: int) -> Dict[str, torch.Tensor]:
+ # -------------------------------- Load hq images -------------------------------- #
+ hq_path = self.paths[index]
+ success = False
+ for _ in range(3):
+ try:
+ pil_img = Image.open(hq_path).convert("RGB")
+ success = True
+ break
+ except:
+ time.sleep(1)
+ assert success, f"failed to load image {hq_path}"
+
+ if self.crop_type == "random":
+ pil_img = random_crop_arr(pil_img, self.out_size)
+ elif self.crop_type == "center":
+ pil_img = center_crop_arr(pil_img, self.out_size)
+ # self.crop_type is "none"
+ else:
+ pil_img = np.array(pil_img)
+ assert pil_img.shape[:2] == (self.out_size, self.out_size)
+ # hwc, rgb to bgr, [0, 255] to [0, 1], float32
+ img_hq = (pil_img[..., ::-1] / 255.0).astype(np.float32)
+
+ # -------------------- Do augmentation for training: flip, rotation -------------------- #
+ img_hq = augment(img_hq, self.use_hflip, self.use_rot)
+
+ # [0, 1], BGR to RGB, HWC to CHW
+ img_hq = torch.from_numpy(
+ img_hq[..., ::-1].transpose(2, 0, 1).copy()
+ ).float()
+
+ return {
+ "hq": img_hq,
+ 'txt': ""
+ }
+
+ def __len__(self) -> int:
+ return len(self.paths)
diff --git a/dataset/codeformer.py b/dataset/codeformer.py
new file mode 100644
index 0000000..f002d86
--- /dev/null
+++ b/dataset/codeformer.py
@@ -0,0 +1,109 @@
+from typing import Sequence, Dict, Union
+import math
+import time
+
+import numpy as np
+import cv2
+from PIL import Image
+import torch.utils.data as data
+
+from utils.file import load_file_list
+from utils.image import center_crop_arr, augment, random_crop_arr
+from utils.degradation import (
+ random_mixed_kernels, random_add_gaussian_noise, random_add_jpg_compression
+)
+
+
+class CodeformerDataset(data.Dataset):
+
+ def __init__(
+ self,
+ file_list: str,
+ out_size: int,
+ crop_type: str,
+ use_hflip: bool,
+ blur_kernel_size: int,
+ kernel_list: Sequence[str],
+ kernel_prob: Sequence[float],
+ blur_sigma: Sequence[float],
+ downsample_range: Sequence[float],
+ noise_range: Sequence[float],
+ jpeg_range: Sequence[int]
+ ) -> "CodeformerDataset":
+ super(CodeformerDataset, self).__init__()
+ self.file_list = file_list
+ self.paths = load_file_list(file_list)
+ self.out_size = out_size
+ self.crop_type = crop_type
+ assert self.crop_type in ["none", "center", "random"]
+ self.use_hflip = use_hflip
+ # degradation configurations
+ self.blur_kernel_size = blur_kernel_size
+ self.kernel_list = kernel_list
+ self.kernel_prob = kernel_prob
+ self.blur_sigma = blur_sigma
+ self.downsample_range = downsample_range
+ self.noise_range = noise_range
+ self.jpeg_range = jpeg_range
+
+ def __getitem__(self, index: int) -> Dict[str, Union[np.ndarray, str]]:
+ # load gt image
+ # Shape: (h, w, c); channel order: BGR; image range: [0, 1], float32.
+ gt_path = self.paths[index]
+ success = False
+ for _ in range(3):
+ try:
+ pil_img = Image.open(gt_path).convert("RGB")
+ success = True
+ break
+ except:
+ time.sleep(1)
+ assert success, f"failed to load image {gt_path}"
+
+ if self.crop_type == "center":
+ pil_img_gt = center_crop_arr(pil_img, self.out_size)
+ elif self.crop_type == "random":
+ pil_img_gt = random_crop_arr(pil_img, self.out_size)
+ else:
+ pil_img_gt = np.array(pil_img)
+ assert pil_img_gt.shape[:2] == (self.out_size, self.out_size)
+ img_gt = (pil_img_gt[..., ::-1] / 255.0).astype(np.float32)
+
+ # random horizontal flip
+ img_gt = augment(img_gt, hflip=self.use_hflip, rotation=False, return_status=False)
+ h, w, _ = img_gt.shape
+
+ # ------------------------ generate lq image ------------------------ #
+ # blur
+ kernel = random_mixed_kernels(
+ self.kernel_list,
+ self.kernel_prob,
+ self.blur_kernel_size,
+ self.blur_sigma,
+ self.blur_sigma,
+ [-math.pi, math.pi],
+ noise_range=None
+ )
+ img_lq = cv2.filter2D(img_gt, -1, kernel)
+ # downsample
+ scale = np.random.uniform(self.downsample_range[0], self.downsample_range[1])
+ img_lq = cv2.resize(img_lq, (int(w // scale), int(h // scale)), interpolation=cv2.INTER_LINEAR)
+ # noise
+ if self.noise_range is not None:
+ img_lq = random_add_gaussian_noise(img_lq, self.noise_range)
+ # jpeg compression
+ if self.jpeg_range is not None:
+ img_lq = random_add_jpg_compression(img_lq, self.jpeg_range)
+
+ # resize to original size
+ img_lq = cv2.resize(img_lq, (w, h), interpolation=cv2.INTER_LINEAR)
+
+ # BGR to RGB, [-1, 1]
+ target = (img_gt[..., ::-1] * 2 - 1).astype(np.float32)
+ # BGR to RGB, [0, 1]
+ source = img_lq[..., ::-1].astype(np.float32)
+
+ return dict(jpg=target, txt="", hint=source)
+
+ def __len__(self) -> int:
+ return len(self.paths)
diff --git a/dataset/data_module.py b/dataset/data_module.py
new file mode 100644
index 0000000..d4dcde3
--- /dev/null
+++ b/dataset/data_module.py
@@ -0,0 +1,68 @@
+from typing import Any, Tuple, Mapping
+
+from pytorch_lightning.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS
+import pytorch_lightning as pl
+from torch.utils.data import DataLoader, Dataset
+from omegaconf import OmegaConf
+
+from utils.common import instantiate_from_config
+from dataset.batch_transform import BatchTransform, IdentityBatchTransform
+
+
+class BIRDataModule(pl.LightningDataModule):
+
+ def __init__(
+ self,
+ train_config: str,
+ val_config: str=None
+ ) -> "BIRDataModule":
+ super().__init__()
+ self.train_config = OmegaConf.load(train_config)
+ self.val_config = OmegaConf.load(val_config) if val_config else None
+
+ def load_dataset(self, config: Mapping[str, Any]) -> Tuple[Dataset, BatchTransform]:
+ dataset = instantiate_from_config(config["dataset"])
+ batch_transform = (
+ instantiate_from_config(config["batch_transform"])
+ if config.get("batch_transform") else IdentityBatchTransform()
+ )
+ return dataset, batch_transform
+
+ def setup(self, stage: str) -> None:
+ if stage == "fit":
+ self.train_dataset, self.train_batch_transform = self.load_dataset(self.train_config)
+ if self.val_config:
+ self.val_dataset, self.val_batch_transform = self.load_dataset(self.val_config)
+ else:
+ self.val_dataset, self.val_batch_transform = None, None
+ else:
+ raise NotImplementedError(stage)
+
+ def train_dataloader(self) -> TRAIN_DATALOADERS:
+ return DataLoader(
+ dataset=self.train_dataset, **self.train_config["data_loader"]
+ )
+
+ def val_dataloader(self) -> EVAL_DATALOADERS:
+ if self.val_dataset is None:
+ return None
+ return DataLoader(
+ dataset=self.val_dataset, **self.val_config["data_loader"]
+ )
+
+ def on_after_batch_transfer(self, batch: Any, dataloader_idx: int) -> Any:
+ self.trainer: pl.Trainer
+
+ if self.trainer.training:
+ return self.train_batch_transform(batch)
+ elif self.trainer.validating or self.trainer.sanity_checking:
+ return self.val_batch_transform(batch)
+ else:
+ raise RuntimeError(
+ "Trainer state: \n"
+ f"training: {self.trainer.training}\n"
+ f"validating: {self.trainer.validating}\n"
+ f"testing: {self.trainer.testing}\n"
+ f"predicting: {self.trainer.predicting}\n"
+ f"sanity_checking: {self.trainer.sanity_checking}"
+ )
diff --git a/dataset/projectdata.py b/dataset/projectdata.py
new file mode 100644
index 0000000..3e139be
--- /dev/null
+++ b/dataset/projectdata.py
@@ -0,0 +1,121 @@
+from typing import Dict, Sequence
+import math
+import random
+import time
+import glob
+import os
+import cv2
+
+import numpy as np
+import torch
+from torch.utils import data
+from PIL import Image
+
+from utils.degradation import circular_lowpass_kernel, random_mixed_kernels
+from utils.image import augment, random_crop_arr, center_crop_arr
+from utils.file import load_file_list
+
+
+class PROJECTDataset(data.Dataset):
+ """
+ # TODO: add comment
+ """
+
+ def __init__(self,
+ file_path: list,
+ out_size: int,
+ scale: int,
+ hr_pattern: str,
+ blur_sigma: float,
+ blur_kernel: int,
+ crop_type: str,
+ use_hflip: bool,
+ use_rot: bool,
+ ):
+ super().__init__()
+
+ self.patch_size = out_size // 4
+ self.scale = scale
+
+ lq_files = []
+ hr_files = []
+ for path in file_path:
+ lq_files += glob.glob(os.path.join(path, "*phone.*"))
+ hr_files += glob.glob(os.path.join(path, "*screen.*"))
+
+ self.lq_files = lq_files
+ self.hr_files = hr_files
+ self.lq_files.sort()
+ self.hr_files.sort()
+
+ self.hr_pattern = hr_pattern
+ self.blur_sigma = blur_sigma
+ self.blur_kernel = blur_kernel
+ self.noise_scale1 = 0.002
+ self.noise_scale2 = 0.003
+ self.blur_kernal_list = [ i for i in range(1, self.blur_kernel * 2, 2)]
+ self.blur_sigma_list = [ i / 10 for i in range(1, int(2 * self.blur_sigma * 10), 1)]
+
+ self.edge_pixel = 0
+
+ @torch.no_grad()
+ def __len__(self):
+ return len(self.lq_files)
+
+ def __getitem__(self, index):
+ lowRes_file = self.lq_files[index]
+ highRes_file = self.hr_files[index]
+
+ hightRes_img = Image.open(highRes_file).convert("RGB")
+ hightRes_img = np.array(hightRes_img)
+ hightRes_img = (hightRes_img[..., ::-1] / 255.0).astype(np.float32)
+ lowRes_img = Image.open(lowRes_file).convert("RGB")
+ lowRes_img = np.array(lowRes_img)
+ lowRes_img = (lowRes_img[..., ::-1] / 255.0).astype(np.float32)
+
+ low_h = lowRes_img.shape[0]
+ high_h = hightRes_img.shape[0]
+ if low_h == high_h:
+ lowRes_img = cv2.resize(lowRes_img, None, fx=1 / self.scale, fy=1 / self.scale)
+
+ # scale为4数据集用作2x训练
+ if high_h // low_h == 4 and self.scale == 2:
+ hightRes_img = cv2.resize(hightRes_img, None, fx=1 / 2, fy=1 / 2)
+
+ # scale为2数据集用作4x训练
+ if "20231021_plant_lightroom_Crop320" in lowRes_file and self.scale == 4:
+ lowRes_img = cv2.resize(lowRes_img, None, fx=1 / 2, fy=1 / 2)
+
+
+ img_size_ori_h, img_size_ori_w = lowRes_img.shape[:2]
+ i = random.randint(self.edge_pixel, img_size_ori_h - self.patch_size - self.edge_pixel)
+ j = random.randint(self.edge_pixel, img_size_ori_w - self.patch_size - self.edge_pixel)
+ hightRes_img = hightRes_img[i * self.scale : i * self.scale + self.patch_size * self.scale, j * self.scale : j * self.scale + self.patch_size * self.scale]
+ lowRes_img = lowRes_img[i : i + self.patch_size, j : j + self.patch_size]
+
+ if random.uniform(0, 1) < 0.5:
+ hightRes_img = hightRes_img[::-1]
+ lowRes_img = lowRes_img[::-1]
+ if random.uniform(0, 1) < 0.5:
+ hightRes_img = hightRes_img[:,::-1]
+ lowRes_img = lowRes_img[:,::-1]
+ if 1:
+ sigma_x_offset = random.choice(self.blur_sigma_list)
+ sigma_y_offset = random.choice(self.blur_sigma_list)
+ kernal_x_offset = random.choice(self.blur_kernal_list)
+ kernal_y_offset = random.choice(self.blur_kernal_list)
+ lowRes_img = cv2.GaussianBlur(lowRes_img, (kernal_x_offset, kernal_y_offset), sigmaX=sigma_x_offset, sigmaY=sigma_y_offset)
+ # lowRes_img = lowRes_img * np.random.normal(loc=1, scale=self.noise_scale1, size=lowRes_img.shape) + np.random.normal(loc=0, scale=self.noise_scale2, size=lowRes_img.shape)
+
+ hightRes_img = np.ascontiguousarray(hightRes_img)
+ lowRes_img = np.ascontiguousarray(lowRes_img)
+
+ # lowRes_img = lowRes_img[:,:,None]
+ # hightRes_img = hightRes_img[:,:,None]
+ lowRes_img = lowRes_img.clip(0, 1)
+ hightRes_img = hightRes_img.clip(0, 1)
+
+ lowRes_img = torch.from_numpy(np.ascontiguousarray(np.transpose(np.stack(lowRes_img, axis=0), (2, 0, 1)))).float()
+ hightRes_img = torch.from_numpy(np.ascontiguousarray(np.transpose(np.stack(hightRes_img, axis=0), (2, 0, 1)))).float()
+
+ return {'lq': lowRes_img, 'hq': hightRes_img, 'txt': '', 'file_name': lowRes_file}
\ No newline at end of file
diff --git a/dataset/realesrgan.py b/dataset/realesrgan.py
new file mode 100644
index 0000000..2a4a812
--- /dev/null
+++ b/dataset/realesrgan.py
@@ -0,0 +1,183 @@
+from typing import Dict, Sequence
+import math
+import random
+import time
+
+import numpy as np
+import torch
+from torch.utils import data
+from PIL import Image
+
+from utils.degradation import circular_lowpass_kernel, random_mixed_kernels
+from utils.image import augment, random_crop_arr, center_crop_arr
+from utils.file import load_file_list
+
+
+class RealESRGANDataset(data.Dataset):
+ """
+ # TODO: add comment
+ """
+
+ def __init__(
+ self,
+ file_list: str,
+ out_size: int,
+ crop_type: str,
+ use_hflip: bool,
+ use_rot: bool,
+ # blur kernel settings of the first degradation stage
+ blur_kernel_size: int,
+ kernel_list: Sequence[str],
+ kernel_prob: Sequence[float],
+ blur_sigma: Sequence[float],
+ betag_range: Sequence[float],
+ betap_range: Sequence[float],
+ sinc_prob: float,
+ # blur kernel settings of the second degradation stage
+ blur_kernel_size2: int,
+ kernel_list2: Sequence[str],
+ kernel_prob2: Sequence[float],
+ blur_sigma2: Sequence[float],
+ betag_range2: Sequence[float],
+ betap_range2: Sequence[float],
+ sinc_prob2: float,
+ final_sinc_prob: float
+ ) -> "RealESRGANDataset":
+ super(RealESRGANDataset, self).__init__()
+ self.paths = load_file_list(file_list)
+ self.out_size = out_size
+ self.crop_type = crop_type
+ assert self.crop_type in ["center", "random", "none"], f"invalid crop type: {self.crop_type}"
+
+ self.blur_kernel_size = blur_kernel_size
+ self.kernel_list = kernel_list
+ # a list for each kernel probability
+ self.kernel_prob = kernel_prob
+ self.blur_sigma = blur_sigma
+ # betag used in generalized Gaussian blur kernels
+ self.betag_range = betag_range
+ # betap used in plateau blur kernels
+ self.betap_range = betap_range
+ # the probability for sinc filters
+ self.sinc_prob = sinc_prob
+
+ self.blur_kernel_size2 = blur_kernel_size2
+ self.kernel_list2 = kernel_list2
+ self.kernel_prob2 = kernel_prob2
+ self.blur_sigma2 = blur_sigma2
+ self.betag_range2 = betag_range2
+ self.betap_range2 = betap_range2
+ self.sinc_prob2 = sinc_prob2
+
+ # a final sinc filter
+ self.final_sinc_prob = final_sinc_prob
+
+ self.use_hflip = use_hflip
+ self.use_rot = use_rot
+
+ # kernel size ranges from 7 to 21
+ self.kernel_range = [2 * v + 1 for v in range(3, 11)]
+ # TODO: kernel range is now hard-coded, should be in the configure file
+ # convolving with pulse tensor brings no blurry effect
+ self.pulse_tensor = torch.zeros(21, 21).float()
+ self.pulse_tensor[10, 10] = 1
+
+ @torch.no_grad()
+ def __getitem__(self, index: int) -> Dict[str, torch.Tensor]:
+ # -------------------------------- Load hq images -------------------------------- #
+ hq_path = self.paths[index]
+ success = False
+ for _ in range(3):
+ try:
+ pil_img = Image.open(hq_path).convert("RGB")
+ success = True
+ break
+ except:
+ time.sleep(1)
+ assert success, f"failed to load image {hq_path}"
+
+ if self.crop_type == "random":
+ pil_img = random_crop_arr(pil_img, self.out_size)
+ elif self.crop_type == "center":
+ pil_img = center_crop_arr(pil_img, self.out_size)
+ # self.crop_type is "none"
+ else:
+ pil_img = np.array(pil_img)
+ assert pil_img.shape[:2] == (self.out_size, self.out_size)
+ # hwc, rgb to bgr, [0, 255] to [0, 1], float32
+ img_hq = (pil_img[..., ::-1] / 255.0).astype(np.float32)
+
+ # -------------------- Do augmentation for training: flip, rotation -------------------- #
+ img_hq = augment(img_hq, self.use_hflip, self.use_rot)
+
+ # ------------------------ Generate kernels (used in the first degradation) ------------------------ #
+ kernel_size = random.choice(self.kernel_range)
+ if np.random.uniform() < self.sinc_prob:
+ # this sinc filter setting is for kernels ranging from [7, 21]
+ if kernel_size < 13:
+ omega_c = np.random.uniform(np.pi / 3, np.pi)
+ else:
+ omega_c = np.random.uniform(np.pi / 5, np.pi)
+ kernel = circular_lowpass_kernel(omega_c, kernel_size, pad_to=False)
+ else:
+ kernel = random_mixed_kernels(
+ self.kernel_list,
+ self.kernel_prob,
+ kernel_size,
+ self.blur_sigma,
+ self.blur_sigma, [-math.pi, math.pi],
+ self.betag_range,
+ self.betap_range,
+ noise_range=None
+ )
+ # pad kernel
+ pad_size = (21 - kernel_size) // 2
+ kernel = np.pad(kernel, ((pad_size, pad_size), (pad_size, pad_size)))
+
+ # ------------------------ Generate kernels (used in the second degradation) ------------------------ #
+ kernel_size = random.choice(self.kernel_range)
+ if np.random.uniform() < self.sinc_prob2:
+ if kernel_size < 13:
+ omega_c = np.random.uniform(np.pi / 3, np.pi)
+ else:
+ omega_c = np.random.uniform(np.pi / 5, np.pi)
+ kernel2 = circular_lowpass_kernel(omega_c, kernel_size, pad_to=False)
+ else:
+ kernel2 = random_mixed_kernels(
+ self.kernel_list2,
+ self.kernel_prob2,
+ kernel_size,
+ self.blur_sigma2,
+ self.blur_sigma2, [-math.pi, math.pi],
+ self.betag_range2,
+ self.betap_range2,
+ noise_range=None
+ )
+
+ # pad kernel
+ pad_size = (21 - kernel_size) // 2
+ kernel2 = np.pad(kernel2, ((pad_size, pad_size), (pad_size, pad_size)))
+
+ # ------------------------------------- the final sinc kernel ------------------------------------- #
+ if np.random.uniform() < self.final_sinc_prob:
+ kernel_size = random.choice(self.kernel_range)
+ omega_c = np.random.uniform(np.pi / 3, np.pi)
+ sinc_kernel = circular_lowpass_kernel(omega_c, kernel_size, pad_to=21)
+ sinc_kernel = torch.FloatTensor(sinc_kernel)
+ else:
+ sinc_kernel = self.pulse_tensor
+
+ # [0, 1], BGR to RGB, HWC to CHW
+ img_hq = torch.from_numpy(
+ img_hq[..., ::-1].transpose(2, 0, 1).copy()
+ ).float()
+ kernel = torch.FloatTensor(kernel)
+ kernel2 = torch.FloatTensor(kernel2)
+
+ return {
+ "hq": img_hq, "kernel1": kernel, "kernel2": kernel2,
+ "sinc_kernel": sinc_kernel, "txt": ""
+ }
+
+ def __len__(self) -> int:
+ return len(self.paths)
diff --git a/figs/logo.png b/figs/logo.png
index 79c46f3fefbfc5678c44d2bd1e7d3bb8c254b6a0..aca7662a8d41a7949ff751c848cbf5f70dfae85e 100644
GIT binary patch
literal 30467
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