├── README.md ├── generate_list.py ├── rnn_fcn_eval.py ├── data_provider.py ├── U_Net.py ├── raw_image.py ├── rnn_fcn_train.py ├── arch.py └── mosaic_utils.py /README.md: -------------------------------------------------------------------------------- 1 | # Burst image denoising using a RNN based CNN architechture. 2 | 3 | This is the re-implement of the paper `End-to-End Denoising of Dark Burst Images Using Recurrent Fully Convolutional Networks` accessing at https://arxiv.org/abs/1904.07483. 4 | 5 | **TODO:upload the code of training on the Vimeo-90K dataset** 6 | 7 | All codes are implemented by PyTorch 1.0.0 and Numpy. 8 | 9 | 10 | In my simulations, the dataset selected is CID Dataset (Learning to See in the Dark). 11 | 12 | Before to train a model, the `train_list.txt` and `val_list.txt` should be generated by the python code `generate_list.py`, and this code has included in the train code 'rnn_fcn_train.py'. 13 | 14 | #### If you like the codes or they are helpful for you, give a `Start` or `Fork` to support me. 15 | -------------------------------------------------------------------------------- /generate_list.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | def generate_list(path, out_path, burst=8): 5 | path_train = os.path.join(path, 'short') 6 | path_gt = os.path.join(path, 'long') 7 | f_train = open(os.path.join(out_path, 'train_list.txt'), 'w') 8 | f_test = open(os.path.join(out_path, 'test_list.txt'), 'w') 9 | files = os.listdir(path_train) 10 | files_gt = os.listdir(path_gt) 11 | files_set = set() 12 | 13 | for file in files: 14 | files_set.add(file[:5]) 15 | 16 | len_file = len(files_set) 17 | train_id = np.random.permutation(len_file)[:200] 18 | 19 | for index, id in enumerate(files_set): 20 | t = [] 21 | flag = False 22 | for i in range(len(files)): 23 | if id in files[i]: 24 | t.append(i) 25 | flag = True 26 | else: 27 | flag = False 28 | if len(t) > 0 and not flag: 29 | break 30 | rand = np.random.permutation(len(t))[:burst] 31 | f_temp = [] 32 | for r in rand: 33 | f_temp.append(os.path.join('short', files[t[r]])) 34 | for file in files_gt: 35 | if id in file: 36 | f_temp.append(os.path.join('long', file)) 37 | 38 | if index in train_id: 39 | f_train.write(' '.join(f_temp) + '\n') 40 | else: 41 | f_test.write(' '.join(f_temp) + '\n') 42 | break 43 | f_train.close() 44 | f_test.close() 45 | 46 | print('Process of generating train/test file list is ok!') 47 | 48 | if __name__ == '__main__': 49 | dataset_path = 'G:/BinZhang/DataSets/rnn_fcn/Sony/Sony' 50 | out_path = './' 51 | generate_list(dataset_path, out_path) -------------------------------------------------------------------------------- /rnn_fcn_eval.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | from generate_list import generate_list 4 | from data_provider import * 5 | from rnn_fcn_train import sampler 6 | from torch.utils.data import DataLoader 7 | from arch import Deep_Burst_Denoise 8 | import torchvision.transforms as transfroms 9 | from PIL import Image 10 | 11 | def rnn_fcn_eval(dataset_path, txt_file, patch_size=512, model_file=None): 12 | # 训练模式下才会输出gt 13 | dataset = Data_Provider(dataset_path, txt_file, patch_size, True) 14 | data_loader = DataLoader( 15 | dataset, 1, sampler=sampler(1, dataset), num_workers=4 16 | ) 17 | # load the model 18 | model = Deep_Burst_Denoise(1, 3).cuda() 19 | model.load_state_dict(torch.load(model_file)) 20 | print('Model load OK!') 21 | # 22 | trans_tensor_rgb = transforms.ToPILImage() 23 | # 24 | model.eval() 25 | data_loader = iter(data_loader) 26 | with torch.no_grad(): 27 | for i in range(10): 28 | train_data, gt = next(data_loader) 29 | train_data = train_data.cuda() 30 | gt = trans_tensor_rgb(gt.squeeze()) 31 | gt.save('./eval_imgs/{}_gt.png'.format(i), quality=100) 32 | for channel in range(train_data.size(1)): 33 | if channel == 0: 34 | sfn_out, mfn_out, mfn_f = model(train_data[:, channel, ...].unsqueeze(1)) 35 | else: 36 | sfn_out, mfn_out, mfn_f = model(train_data[:, channel, ...].unsqueeze(1), mfn_f) 37 | sfn_out = sfn_out.squeeze().detach().cpu() 38 | mfn_out = mfn_out.squeeze().detach().cpu() 39 | sfn_img = trans_tensor_rgb(sfn_out) 40 | mfn_img = trans_tensor_rgb(mfn_out) 41 | sfn_img.save('./eval_imgs/{}_sfn_{}.png'.format(i, channel), quality=100) 42 | mfn_img.save('./eval_imgs/{}_mfn_{}.png'.format(i, channel), quality=100) 43 | 44 | del sfn_out, mfn_out, sfn_img, mfn_img 45 | 46 | print('Save image of step {} at channel {} is OK!'.format(i, channel)) 47 | 48 | def plot_loss(path): 49 | import matplotlib.pyplot as plt 50 | loss = np.fromfile(path, np.float32, sep=',') 51 | plt.plot(loss) 52 | plt.show() 53 | 54 | if __name__ == '__main__': 55 | # dataset_path = 'G:\\BinZhang\\DataSets\\rnn_fcn\\Sony\\Sony' 56 | # rnn_fcn_eval( 57 | # dataset_path=dataset_path, 58 | # txt_file='./test_list.txt', 59 | # patch_size=512, 60 | # model_file='./model/model_newest.pkl' 61 | # ) 62 | plot_loss('./model/loss_log.txt') -------------------------------------------------------------------------------- /data_provider.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import rawpy 3 | from torch.utils.data import Dataset 4 | import numpy as np 5 | import torchvision.transforms as transforms 6 | import os 7 | from PIL import Image 8 | 9 | class Data_Provider(Dataset): 10 | def __init__(self, base_path, txt_file, patch_size=512, train=True): 11 | """ 12 | 给定txt文件,文件内包括用于训练/测试的数据文件名 13 | 如果为训练,则每行包含多个文件名,最后一个为ground of truth 14 | 如果为测试,则每行同样包含多个文件名,不包含ground of truth 15 | :param txt_file: 16 | """ 17 | super(Data_Provider, self).__init__() 18 | self.base_path = base_path 19 | self.patch_size = patch_size 20 | with open(txt_file, 'r') as txt: 21 | self.filename = txt.readlines() 22 | self.train = train 23 | 24 | def __getitem__(self, index): 25 | """ 26 | 获取index对应的数据 27 | :param index: 28 | :return: 29 | """ 30 | # 得到每个文件名 31 | files = self.filename[index].split() 32 | if self.train: 33 | files, gt_file = files[:-1], files[-1] 34 | raws = [] 35 | for file in files: 36 | raws.append(torch.from_numpy(get_RAW_from_file(os.path.join(self.base_path, file)))) 37 | raws = torch.stack(raws, dim=0) 38 | height, width = raws[0].size() # 获取宽和高 39 | max_crop_height = height - self.patch_size 40 | max_crop_width = width - self.patch_size 41 | crop_h, crop_w = np.random.randint(0, max_crop_height, 1)[0], np.random.randint(0, max_crop_width, 1)[0] 42 | train_data = raws[:, crop_h:crop_h+self.patch_size, crop_w:crop_w+self.patch_size] 43 | if not self.train: 44 | return train_data 45 | gt = get_sRGB_from_file(os.path.join(self.base_path, gt_file)) 46 | gt = gt[crop_h:crop_h+self.patch_size, crop_w:crop_w+self.patch_size, :] 47 | gt = Image.fromarray(gt) 48 | # gt.show() 49 | gt = transforms.ToTensor()(gt) 50 | return train_data, gt 51 | 52 | def __len__(self): 53 | return len(self.filename) 54 | 55 | @staticmethod 56 | def get_rand_position(self): 57 | pass 58 | 59 | def get_sRGB_from_file(filename): 60 | """ 61 | 读取RAW文件并转为sRGB图像, H*W*3 62 | :param filename: 63 | :return: 64 | """ 65 | srgb = None 66 | try: 67 | raw = rawpy.imread(filename) 68 | srgb = raw.postprocess() 69 | except Exception: 70 | pass 71 | finally: 72 | return srgb 73 | 74 | def get_RAW_from_file(filename): 75 | RAW = None 76 | try: 77 | raw = rawpy.imread(filename) 78 | # 需要减去黑色电平 79 | bl = raw.black_level_per_channel 80 | RAW = raw.raw_image_visible.astype(np.float32) 81 | # color_desc = raw.color_desc.reshape(-1) # color的顺序 82 | # color_pattern = str(raw.color_pattern, encoding='gbk') 83 | # for index, (x, y) in zip(range(4), [(0, 0), (0, 1), (1, 0), (1, 1)]): 84 | # index_cur = color_pattern[index] 85 | # raw_data[x::2, y::2] -= bl[index_cur] 86 | # RAW = np.clip(0, 65535) 87 | except Exception: 88 | pass 89 | finally: 90 | return RAW 91 | 92 | if __name__ == '__main__': 93 | dp = Data_Provider('./', 'test.txt') 94 | dp = iter(dp) 95 | a,b = next(dp) 96 | print(a.size(), b.size()) 97 | -------------------------------------------------------------------------------- /U_Net.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | """ 6 | U-Net中连续两个Conv+BN+ReLU组合 7 | """ 8 | class Double_Conv(nn.Module): 9 | def __init__(self, in_channel, out_channel, sepConv=False): 10 | super(Double_Conv, self).__init__() 11 | if sepConv: 12 | self.conv = nn.Sequential( 13 | nn.Conv2d(in_channel, in_channel, 3, 1, 1), 14 | nn.Conv2d(in_channel, out_channel, 1, 1, 0), 15 | nn.BatchNorm2d(out_channel), 16 | nn.ReLU(inplace=True), 17 | nn.Conv2d(out_channel, out_channel, 3, 1, 1), 18 | nn.BatchNorm2d(out_channel), 19 | nn.ReLU(inplace=True) 20 | ) 21 | else: 22 | self.conv = nn.Sequential( 23 | nn.Conv2d(in_channel, out_channel, 3, 1, 1), 24 | nn.BatchNorm2d(out_channel), 25 | nn.ReLU(inplace=True), 26 | nn.Conv2d(out_channel, out_channel, 3, 1, 1), 27 | nn.BatchNorm2d(out_channel), 28 | nn.ReLU(inplace=True) 29 | ) 30 | 31 | def forward(self, data): 32 | return self.conv(data) 33 | # class Double_Conv(nn.Module): 34 | # def __init__(self, in_channel, out_channel): 35 | # super(Double_Conv, self).__init__() 36 | # self.conv = nn.Sequential( 37 | # nn.Conv2d(in_channel, out_channel, 3, 1, 1), 38 | # nn.BatchNorm2d(out_channel), 39 | # nn.ReLU(inplace=True), 40 | # nn.Conv2d(out_channel, out_channel, 3, 1, 1), 41 | # nn.BatchNorm2d(out_channel), 42 | # nn.ReLU(inplace=True) 43 | # ) 44 | # 45 | # def forward(self, data): 46 | # return self.conv(data) 47 | 48 | class In_Conv(nn.Module): 49 | def __init__(self, in_channel, out_channel): 50 | super(In_Conv, self).__init__() 51 | self.conv = Double_Conv(in_channel, out_channel, False) 52 | 53 | def forward(self, data): 54 | return self.conv(data) 55 | 56 | class Down(nn.Module): 57 | def __init__(self, in_channel, out_channel): 58 | super(Down, self).__init__() 59 | self.pool = nn.MaxPool2d(2) 60 | self.conv = Double_Conv(in_channel, out_channel, False) 61 | 62 | def forward(self, *data): 63 | if len(data) == 1: 64 | return self.conv(self.pool(data[0])) 65 | # 第二项为已经下采样的部分 66 | elif len(data) == 2: 67 | data0 = self.pool(data[0]) 68 | return self.conv(torch.cat([data0, data[1]], dim=1)) 69 | 70 | class Up(nn.Module): 71 | def __init__(self, in_channel_last, in_channel_toal, out_channel, bilinear=True): 72 | super(Up, self).__init__() 73 | if bilinear: 74 | self.up = F.interpolate 75 | else: 76 | self.up = nn.ConvTranspose2d(in_channel_last//2, in_channel_last//2, 2, stride=2) 77 | self.conv1 = nn.Conv2d(in_channel_last, in_channel_last//2, 3, 1, 1) 78 | self.conv = Double_Conv(in_channel_toal, out_channel) 79 | 80 | def forward(self, data_cur, data_pre, bilinear=True): 81 | if bilinear: 82 | data_cur = self.up(data_cur, scale_factor=2, mode='bilinear') 83 | else: 84 | data_cur = self.up(data_cur) 85 | data_cur = self.conv1(data_cur) 86 | # 从channel维度连接在一起 87 | return self.conv(torch.cat([data_cur, data_pre], dim=1)) 88 | 89 | class Out_Conv(nn.Module): 90 | def __init__(self, in_channel, out_channel): 91 | super(Out_Conv, self).__init__() 92 | self.conv = nn.Conv2d(in_channel, out_channel, 1) 93 | 94 | def forward(self, data): 95 | return self.conv(data) 96 | -------------------------------------------------------------------------------- /raw_image.py: -------------------------------------------------------------------------------- 1 | import rawpy 2 | import numpy as np 3 | from mosaic_utils import * 4 | import torch 5 | from skimage.color import * 6 | from PIL import Image 7 | 8 | class RAW(): 9 | def __init__(self, raw_image=None, pattern=None, black_level=None, white_balance=None, color_correction_matrix=None, 10 | tone_curve=None, rgb_xyz_matrix=None): 11 | self.raw_image = raw_image 12 | self.pattern = pattern 13 | self.black_level = black_level 14 | self.white_balance = white_balance 15 | self.ccm = color_correction_matrix 16 | self.tone_curve = tone_curve 17 | self.rgb_xyz = rgb_xyz_matrix 18 | 19 | def fromFile(self, file_name): 20 | """ 21 | 从文件读取RAW文件 22 | :param file_name: 23 | :return: 24 | """ 25 | raw = rawpy.imread(file_name) 26 | self.raw_image = raw.raw_image_visible.astype(np.float32) 27 | print('min:', np.min(raw.raw_image_visible)) 28 | print('max:', np.max(raw.raw_image_visible)) 29 | order = raw.raw_pattern.reshape(-1) 30 | pattern = str(raw.color_desc, encoding='gbk') 31 | self.pattern = ''.join([pattern[i] for i in order]) 32 | self.black_level = [raw.black_level_per_channel[i] for i in order] 33 | self.white_balance = raw.camera_whitebalance[:3] 34 | self.ccm = None if np.sum(raw.color_matrix[:, :3] == 0) else raw.color_matrix[:, :3] 35 | self.tone_curve = raw.tone_curve 36 | self.rgb_xyz = raw.rgb_xyz_matrix[:3, :] 37 | return self 38 | 39 | def subtract_black(self): 40 | """ 41 | 仅仅减去黑电平 可以用作网络的输入 42 | :return: 43 | """ 44 | # first, subtract the black level from raw image 45 | raw = np.zeros_like(self.raw_image) 46 | for i, (x, y) in zip(range(4), [(0, 0), (0, 1), (1, 0), (1, 1)]): 47 | raw[x::2, y::2] = self.raw_image[x::2, y::2] - self.black_level[i] 48 | return np.clip(raw, 0, 65535) 49 | 50 | def demosaicing(self, bilinear=False): 51 | raw_sub = self.subtract_black() / 65535 52 | print('max_raw_sum', np.max(raw_sub)) 53 | if bilinear: 54 | rgb = demosaicing_bilinear(raw_sub, pattern=self.pattern) 55 | else: 56 | rgb = demosaicing_Malvar2004(raw_sub, pattern=self.pattern) 57 | return np.stack([rgb[:, :, 0]*self.white_balance[0], rgb[:, :, 1]*self.white_balance[1], rgb[:, :, 2]*self.white_balance[2]], axis=-1) 58 | 59 | def get_sRGB(self): 60 | rgb = self.demosaicing(bilinear=False) 61 | print('min:', np.min(rgb)) 62 | print('max:', np.max(rgb)) 63 | rgb = rgb / np.max(rgb) 64 | if not self.ccm is None: 65 | rgb_ccm = convertColor(rgb, self.ccm) 66 | else: 67 | rgb_ccm = rgb 68 | # camera RGB --> xyz 69 | if not self.rgb_xyz is None: 70 | rgb_xyz = convertColor(rgb_ccm, self.rgb_xyz) 71 | else: 72 | rgb_xyz = rgb_ccm 73 | # print('min_ccm',) 74 | # xyz --> sRGB 75 | srgb = xyz2rgb(rgb_xyz) 76 | print('min:', np.min(srgb)) 77 | print('max:', np.max(srgb)) 78 | srgb = np.clip(srgb, 0, 1) 79 | return (srgb*255).astype(np.uint8) 80 | 81 | 82 | def convertColor(img, ccm): 83 | """ 84 | The shape of image should be: C * H * W 85 | :param img: 86 | :param ccm: 87 | :return: 88 | """ 89 | out = np.zeros_like(img) 90 | out[:, :, 0] = ccm[0, 0] * img[:, :, 0] + ccm[0, 1] * img[:, :, 1] + ccm[0, 2] * img[:, :, 2] 91 | out[:, :, 1] = ccm[1, 0] * img[:, :, 0] + ccm[1, 1] * img[:, :, 1] + ccm[1, 2] * img[:, :, 2] 92 | out[:, :, 2] = ccm[2, 0] * img[:, :, 0] + ccm[2, 1] * img[:, :, 1] + ccm[2, 2] * img[:, :, 2] 93 | return out 94 | 95 | if __name__ == '__main__': 96 | raw = RAW() 97 | raw = raw.fromFile('00002_00_10s.ARW') 98 | srgb = raw.get_sRGB() 99 | Image.fromarray(srgb).show() 100 | 101 | raw = rawpy.imread('00002_00_10s.ARW') 102 | srgb = raw.postprocess( 103 | demosaic_algorithm=rawpy.DemosaicAlgorithm.AHD, 104 | half_size=False, 105 | use_camera_wb=True, 106 | output_color=rawpy.ColorSpace.sRGB, 107 | output_bps=8, 108 | no_auto_bright=False, 109 | no_auto_scale=True 110 | ) 111 | Image.fromarray(srgb).show() 112 | 113 | print('Over!') -------------------------------------------------------------------------------- /rnn_fcn_train.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from torch.utils.data import DataLoader, Sampler 3 | import numpy as np 4 | import argparse 5 | import rawpy 6 | from data_provider import * 7 | from arch import Deep_Burst_Denoise 8 | import torch.nn.functional as F 9 | import torch.optim as optim 10 | from generate_list import generate_list 11 | from PIL import Image 12 | 13 | TENSOR_BOARD = False 14 | if TENSOR_BOARD: 15 | from tensorboardX import SummaryWriter 16 | """ 17 | The dataloader sampler, to ensure the size of each batch are same. 18 | """ 19 | class sampler(Sampler): 20 | def __init__(self, batch_size, data_source): 21 | super(sampler, self).__init__(data_source) 22 | self.batch_size = batch_size 23 | self.total_size = len(data_source) 24 | 25 | def __iter__(self): 26 | if self.total_size % self.batch_size == 0: 27 | return iter(torch.randperm(self.total_size)) 28 | else: 29 | return iter(torch.randperm(self.total_size).tolist() + torch.randperm(self.total_size).tolist()[:self.batch_size-self.total_size % self.batch_size]) 30 | 31 | def __len__(self): 32 | return self.total_size 33 | 34 | 35 | def adjust_learning_rate(optimizer, lr): 36 | """Decay the learning rate by one half""" 37 | lr = lr / 2 38 | for param_group in optimizer.param_groups: 39 | param_group['lr'] = lr 40 | return lr 41 | 42 | def rnn_fcn_train(dataset_path, txt_file, batch_size=4, patch_size=512, lr=5e-4, lr_decay=1000, max_epoch=10000): 43 | dataset = Data_Provider(dataset_path, txt_file, patch_size=patch_size, train=True) 44 | data_loader = DataLoader( 45 | dataset=dataset, 46 | batch_size=batch_size, 47 | sampler=sampler(batch_size, dataset), 48 | num_workers=4 49 | ) 50 | # build the architecture 51 | model = Deep_Burst_Denoise(1, 3).cuda() 52 | # switch to the train mode 53 | model.train() 54 | # init the optimizer to train 55 | optimizer = optim.Adam(model.parameters(), lr=lr) 56 | # if tensorboardX is used, saving the loss logs 57 | if TENSOR_BOARD: 58 | summary = SummaryWriter('./logs', comment='loss') 59 | # create a txt file to save loss 60 | loss_file = open('./loss_logs/loss_log.txt', 'w+') 61 | # the variable to save loss during the train process 62 | global_step = 0 63 | # to ensure the losses are saved correctly, initial a max value to min_loss 64 | min_loss = 10**9+7 65 | try: 66 | for epoch in range(max_epoch): 67 | # learning rate decay 68 | if epoch > 0 and epoch % lr_decay == 0: 69 | lr = adjust_learning_rate(optimizer, lr) 70 | print('=============Epoch:{}, lr:{}.============'.format(epoch+1, lr)) 71 | for step, (train_data, gt) in enumerate(data_loader): 72 | train_data = train_data.cuda() 73 | gt = gt.cuda() 74 | loss_temp = 0 75 | for channel in range(train_data.size(1)): 76 | if channel == 0: 77 | sfn_out, mfn_out, mfn_f = model(train_data[:, channel, ...].unsqueeze(1)) 78 | else: 79 | sfn_out, mfn_out, mfn_f = model(train_data[:, channel, ...].unsqueeze(1), mfn_f) 80 | # 计算loss 81 | loss_temp += F.l1_loss(sfn_out, gt) + F.l1_loss(mfn_out, gt) 82 | # 保存loss到Tensorboard 83 | global_step += 1 84 | if TENSOR_BOARD: 85 | summary.add_scalar('loss', loss_temp, global_step) 86 | # 打印信息 87 | print('Epoch:{}, Step:{}, Loss:{:.4f}.'.format(epoch+1, step, loss_temp)) 88 | # 优化 89 | optimizer.zero_grad() 90 | loss_temp.backward() 91 | optimizer.step() 92 | # save some temp images 93 | # if global_step % 20 == 0: 94 | # img = mfn_out.detach() 95 | # summary.add_image('image', img.squeeze(0), global_step) 96 | 97 | # save loss_temp to file 98 | loss_file.write('{},'.format(loss_temp)) 99 | # save the model 100 | if loss_temp < min_loss: 101 | min_loss = loss_temp 102 | torch.save(model.state_dict(), 'model_min_loss.pkl') 103 | if global_step % 1000 == 0: 104 | torch.save(model.state_dict(), 'model_newest.pkl') 105 | finally: 106 | loss_file.close() 107 | 108 | if __name__ == '__main__': 109 | dataset_path = 'G:\\BinZhang\\DataSets\\rnn_fcn\\Sony\\Sony' 110 | generate_list(dataset_path, './', burst=8) 111 | rnn_fcn_train( 112 | dataset_path=dataset_path, 113 | txt_file='./train_list.txt', 114 | batch_size=1, 115 | patch_size=512, 116 | lr=5e-5 117 | ) 118 | -------------------------------------------------------------------------------- /arch.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from U_Net import * 4 | 5 | """ 6 | 单帧去噪采用U-Net结构 7 | """ 8 | class Single_Frame_Net(nn.Module): 9 | def __init__(self, in_channel, out_channel): 10 | super(Single_Frame_Net, self).__init__() 11 | self.inc = In_Conv(in_channel, 64) 12 | self.down1 = Down(64, 128) 13 | self.down2 = Down(128, 256) 14 | self.down3 = Down(256, 512) 15 | self.down4 = Down(512, 1024) 16 | self.up1 = Up(1024, 1024, 512) 17 | self.up2 = Up(512, 512, 256) 18 | self.up3 = Up(256, 256, 128) 19 | self.up4 = Up(128, 128, 64) 20 | self.outc = Out_Conv(64, out_channel) 21 | 22 | def forward(self, data): 23 | """ 24 | The forward function of the SFN network. 25 | :param data: one frame of the input burst 26 | :return: the output of SFN and its features of each layers 27 | """ 28 | f1 = self.inc(data) 29 | f2 = self.down1(f1) 30 | f3 = self.down2(f2) 31 | f4 = self.down3(f3) 32 | f5 = self.down4(f4) 33 | f6 = self.up1(f5, f4) 34 | f7 = self.up2(f6, f3) 35 | f8 = self.up3(f7, f2) 36 | f9 = self.up4(f8, f1) 37 | # feature是要传入Multi_Frame_Net的每层的特征 38 | feature = [f1, f2, f3, f4, f5, f6, f7, f8, f9] 39 | out = self.outc(f9) 40 | return out, feature 41 | 42 | """ 43 | 多帧网络是基于时间维度 44 | 每层的输入为: 45 | 当前时刻: SFN对应层的输出 46 | 前一时刻: MFN对应层的输出 47 | """ 48 | class Multi_Frame_Net(nn.Module): 49 | def __init__(self, out_channel): 50 | super(Multi_Frame_Net, self).__init__() 51 | self.inc = In_Conv(64 + 64, 64) # SFN.inc + MFN.inc(t-1) 52 | self.down1 = Down(64 + 128 + 128, 128) # 53 | self.down2 = Down(128 + 256 + 256, 256) 54 | self.down3 = Down(256 + 512 + 512, 512) 55 | self.down4 = Down(512 + 1024 + 1024, 1024) 56 | self.up1 = Up(1024, 1024 + 512 + 512, 512) 57 | self.up2 = Up(512, 512 + 256 + 256, 256) 58 | self.up3 = Up(256, 256 + 128 + 128, 128) 59 | self.up4 = Up(128, 128 + 64 + 64, 64) 60 | self.outc = Out_Conv(64, out_channel) 61 | 62 | def forward(self, *input): 63 | """ 64 | The forward function of MFN 65 | :param input: one or two, if one, it is the feature of SFN at time instance 1; 66 | otherwise, the inputs are features of SFN and MFN, where MFN is the last time instance. 67 | :return: the output of this MFN and its features of each layers 68 | """ 69 | if len(input) == 1: 70 | f_sfn = input[0] 71 | device = f_sfn[0].device 72 | f1 = self.inc(torch.cat([f_sfn[0], torch.zeros(f_sfn[0].size(), device=device)], dim=1)) 73 | f2 = self.down1(f1, torch.cat([f_sfn[1], torch.zeros(f_sfn[1].size(), device=device)], dim=1)) 74 | f3 = self.down2(f2, torch.cat([f_sfn[2], torch.zeros(f_sfn[2].size(), device=device)], dim=1)) 75 | f4 = self.down3(f3, torch.cat([f_sfn[3], torch.zeros(f_sfn[3].size(), device=device)], dim=1)) 76 | f5 = self.down4(f4, torch.cat([f_sfn[4], torch.zeros(f_sfn[4].size(), device=device)], dim=1)) 77 | f6 = self.up1(f5, torch.cat([f_sfn[5], torch.zeros(f_sfn[5].size(), device=device), f4], dim=1)) 78 | f7 = self.up2(f6, torch.cat([f_sfn[6], torch.zeros(f_sfn[6].size(), device=device), f3], dim=1)) 79 | f8 = self.up3(f7, torch.cat([f_sfn[7], torch.zeros(f_sfn[7].size(), device=device), f2], dim=1)) 80 | f9 = self.up4(f8, torch.cat([f_sfn[8], torch.zeros(f_sfn[8].size(), device=device), f1], dim=1)) 81 | feature = [f1, f2, f3, f4, f5, f6, f7, f8, f9] 82 | else: 83 | f_sfn, f_mfn = input 84 | f1 = self.inc(torch.cat([f_sfn[0], f_mfn[0]], dim=1)) 85 | f2 = self.down1(f1, torch.cat([f_sfn[1], f_mfn[1]], dim=1)) 86 | f3 = self.down2(f2, torch.cat([f_sfn[2], f_mfn[2]], dim=1)) 87 | f4 = self.down3(f3, torch.cat([f_sfn[3], f_mfn[3]], dim=1)) 88 | f5 = self.down4(f4, torch.cat([f_sfn[4], f_mfn[4]], dim=1)) 89 | f6 = self.up1(f5, torch.cat([f_sfn[5], f_mfn[5], f4], dim=1)) 90 | f7 = self.up2(f6, torch.cat([f_sfn[6], f_mfn[6], f3], dim=1)) 91 | f8 = self.up3(f7, torch.cat([f_sfn[7], f_mfn[7], f2], dim=1)) 92 | f9 = self.up4(f8, torch.cat([f_sfn[8], f_mfn[8], f1], dim=1)) 93 | feature = [f1, f2, f3, f4, f5, f6, f7, f8, f9] 94 | # Output 95 | return self.outc(f9), feature 96 | 97 | """ 98 | The full architecture of RNN based FCN 99 | """ 100 | class Deep_Burst_Denoise(nn.Module): 101 | def __init__(self, in_channel, out_channel): 102 | super(Deep_Burst_Denoise, self).__init__() 103 | self.SFN = Single_Frame_Net(in_channel, out_channel) 104 | self.MFN = Multi_Frame_Net(out_channel) 105 | 106 | def forward(self, *input): 107 | """ 108 | 如果是第一个时刻,只输入当前时刻的数据 109 | 否则,还要同时输入mfn上一时刻的数据 110 | :param input: 111 | :return: 112 | """ 113 | if len(input) == 1: 114 | data = input[0] 115 | sfn_out, sfn_f = self.SFN(data) 116 | mfn_out, mfn_f = self.MFN(sfn_f) 117 | else: 118 | data, mfn_f_last = input 119 | sfn_out, sfn_f = self.SFN(data) 120 | mfn_out, mfn_f = self.MFN(sfn_f, mfn_f_last) 121 | return sfn_out, mfn_out, mfn_f 122 | 123 | 124 | if __name__ == '__main__': 125 | from tensorboardX import SummaryWriter 126 | from torchsummary import summary 127 | dbn = Deep_Burst_Denoise(3, 3).cuda() 128 | # inp = torch.rand((1, 3, 224, 224)).cuda() 129 | # with SummaryWriter(comment='SFN') as arch_writer: 130 | # # _, f = sfn(inp) 131 | # arch_writer.add_graph(dbn, inp) 132 | 133 | summary(dbn, (3,224,224), batch_size=1, device='cuda') -------------------------------------------------------------------------------- /mosaic_utils.py: -------------------------------------------------------------------------------- 1 | # import torch 2 | import numpy as np 3 | from scipy.ndimage.filters import convolve, convolve1d 4 | 5 | ''' 6 | 从RAW图像中提取RGB各个分量对应的mask 7 | @param pattern: 'rggb', 'grbg', 'bggr' 8 | ''' 9 | def bayer_CFA_pattern(shape, pattern='rggb'): 10 | pattern = pattern.upper() 11 | bayer_cfa = dict({color: np.zeros(shape) for color in 'RGB'}) 12 | raw = np.ones(shape) 13 | for color, (x, y) in zip(pattern, [(0, 0), (0, 1), (1, 0), (1, 1)]): 14 | bayer_cfa[color][x::2, y::2] = raw[x::2, y::2] 15 | return bayer_cfa['R'].astype(np.float32), bayer_cfa['G'].astype(np.float32), bayer_cfa['B'].astype(np.float32) 16 | 17 | ''' 18 | Bilinear Demosaicing 19 | ''' 20 | def demosaicing_bilinear(raw, pattern='rggb'): 21 | kernel_rb = np.array([ 22 | [1, 2, 1], 23 | [2, 4, 2], 24 | [1, 2, 1] 25 | ]) / 4 26 | kernel_g = np.array([ 27 | [0, 1, 0], 28 | [1, 4, 1], 29 | [0, 1, 0] 30 | ]) / 4 31 | # 获取每个颜色的分量 32 | mask_R, mask_G, mask_B = bayer_CFA_pattern(raw.shape, pattern) 33 | data_R, data_G, data_B = raw*mask_R, raw*mask_B, raw*mask_B 34 | # 双线性插值 demosaicing 35 | data_R = convolve(data_R, kernel_rb) 36 | data_G = convolve(data_G, kernel_g) 37 | data_B = convolve(data_B, kernel_rb) 38 | # if not wb: 39 | # wb = white_balance_simple(data_R, data_G, data_B) 40 | # return the color image 41 | return np.stack([data_R, data_G, data_B], axis=-1) 42 | 43 | """ 44 | AHD demosaicing algorithm 45 | """ 46 | def demosaicing_AHD(raw, pattern='rggb'): 47 | pattern = pattern.upper() 48 | 49 | 50 | """ 51 | Malvar (2004) Bayer CFA Demosaicing 52 | =================================== 53 | *Bayer* CFA (Colour Filter Array) *Malvar (2004)* demosaicing. 54 | References 55 | ---------- 56 | - :cite:`Malvar2004a` : Malvar, H. S., He, L.-W., Cutler, R., & Way, O. M. 57 | (2004). High-Quality Linear Interpolation for Demosaicing of 58 | Bayer-Patterned Color Images. In International Conference of Acoustic, 59 | Speech and Signal Processing (pp. 5-8). 60 | """ 61 | def demosaicing_Malvar2004(CFA, pattern='RGGB'): 62 | """ 63 | Returns the demosaiced *RGB* colourspace array from given *Bayer* CFA using 64 | *Malvar (2004)* demosaicing algorithm. 65 | Parameters 66 | ---------- 67 | CFA : array_like 68 | *Bayer* CFA. 69 | pattern : unicode, optional 70 | **{'RGGB', 'BGGR', 'GRBG', 'GBRG'}**, 71 | Arrangement of the colour filters on the pixel array. 72 | Returns 73 | ------- 74 | ndarray 75 | *RGB* colourspace array. 76 | """ 77 | R_m, G_m, B_m = bayer_CFA_pattern(CFA.shape, pattern) 78 | 79 | GR_GB = np.array( 80 | [[0, 0, -1, 0, 0], 81 | [0, 0, 2, 0, 0], 82 | [-1, 2, 4, 2, -1], 83 | [0, 0, 2, 0, 0], 84 | [0, 0, -1, 0, 0]]) / 8 # yapf: disable 85 | 86 | Rg_RB_Bg_BR = np.array( 87 | [[0, 0, 0.5, 0, 0], 88 | [0, -1, 0, -1, 0], 89 | [-1, 4, 5, 4, - 1], 90 | [0, -1, 0, -1, 0], 91 | [0, 0, 0.5, 0, 0]]) / 8 # yapf: disable 92 | 93 | Rg_BR_Bg_RB = np.transpose(Rg_RB_Bg_BR) 94 | 95 | Rb_BB_Br_RR = np.array( 96 | [[0, 0, -1.5, 0, 0], 97 | [0, 2, 0, 2, 0], 98 | [-1.5, 0, 6, 0, -1.5], 99 | [0, 2, 0, 2, 0], 100 | [0, 0, -1.5, 0, 0]]) / 8 # yapf: disable 101 | 102 | R = CFA * R_m 103 | G = CFA * G_m 104 | B = CFA * B_m 105 | 106 | del G_m 107 | 108 | G = np.where(np.logical_or(R_m == 1, B_m == 1), convolve(CFA, GR_GB), G) 109 | 110 | RBg_RBBR = convolve(CFA, Rg_RB_Bg_BR) 111 | RBg_BRRB = convolve(CFA, Rg_BR_Bg_RB) 112 | RBgr_BBRR = convolve(CFA, Rb_BB_Br_RR) 113 | 114 | del GR_GB, Rg_RB_Bg_BR, Rg_BR_Bg_RB, Rb_BB_Br_RR 115 | 116 | # Red rows. 117 | R_r = np.transpose(np.any(R_m == 1, axis=1)[np.newaxis]) * np.ones(R.shape) 118 | # Red columns. 119 | R_c = np.any(R_m == 1, axis=0)[np.newaxis] * np.ones(R.shape) 120 | # Blue rows. 121 | B_r = np.transpose(np.any(B_m == 1, axis=1)[np.newaxis]) * np.ones(B.shape) 122 | # Blue columns 123 | B_c = np.any(B_m == 1, axis=0)[np.newaxis] * np.ones(B.shape) 124 | 125 | del R_m, B_m 126 | 127 | R = np.where(np.logical_and(R_r == 1, B_c == 1), RBg_RBBR, R) 128 | R = np.where(np.logical_and(B_r == 1, R_c == 1), RBg_BRRB, R) 129 | 130 | B = np.where(np.logical_and(B_r == 1, R_c == 1), RBg_RBBR, B) 131 | B = np.where(np.logical_and(R_r == 1, B_c == 1), RBg_BRRB, B) 132 | 133 | R = np.where(np.logical_and(B_r == 1, B_c == 1), RBgr_BBRR, R) 134 | B = np.where(np.logical_and(R_r == 1, R_c == 1), RBgr_BBRR, B) 135 | 136 | del RBg_RBBR, RBg_BRRB, RBgr_BBRR, R_r, R_c, B_r, B_c 137 | 138 | # if not wb: 139 | # # 简单的白平衡算法 140 | # wb = white_balance_simple(R, G, B) 141 | 142 | return np.stack([R, G, B], axis=-1) 143 | 144 | 145 | """ 146 | DDFAPD - Menon (2007) Bayer CFA Demosaicing 147 | =========================================== 148 | *Bayer* CFA (Colour Filter Array) DDFAPD - *Menon (2007)* demosaicing. 149 | References 150 | ---------- 151 | - :cite:`Menon2007c` : Menon, D., Andriani, S., & Calvagno, G. (2007). 152 | Demosaicing With Directional Filtering and a posteriori Decision. IEEE 153 | Transactions on Image Processing, 16(1), 132-141. 154 | doi:10.1109/TIP.2006.884928 155 | """ 156 | def _cnv_h(x, y): 157 | """ 158 | Helper function for horizontal convolution. 159 | """ 160 | return convolve1d(x, y, mode='mirror') 161 | 162 | def _cnv_v(x, y): 163 | """ 164 | Helper function for vertical convolution. 165 | """ 166 | return convolve1d(x, y, mode='mirror', axis=0) 167 | 168 | def demosaicing_Menon2007(CFA, wb=(1.0, 1.0, 1.0), pattern='RGGB', refining_step=True): 169 | """ 170 | Returns the demosaiced *RGB* colourspace array from given *Bayer* CFA using 171 | DDFAPD - *Menon (2007)* demosaicing algorithm. 172 | Parameters 173 | ---------- 174 | CFA : array_like 175 | *Bayer* CFA. 176 | pattern : unicode, optional 177 | **{'RGGB', 'BGGR', 'GRBG', 'GBRG'}**, 178 | Arrangement of the colour filters on the pixel array. 179 | refining_step : bool 180 | Perform refining step. 181 | Returns 182 | ------- 183 | ndarray 184 | *RGB* colourspace array. 185 | """ 186 | 187 | # 测试 是不是没减去black_level 188 | # CFA -= np.min(CFA) 189 | 190 | R_m, G_m, B_m = bayer_CFA_pattern(CFA.shape, pattern) 191 | 192 | h_0 = np.array([0, 0.5, 0, 0.5, 0]) 193 | h_1 = np.array([-0.25, 0, 0.5, 0, -0.25]) 194 | 195 | R = CFA * R_m 196 | G = CFA * G_m 197 | B = CFA * B_m 198 | 199 | G_H = np.where(G_m == 0, _cnv_h(CFA, h_0) + _cnv_h(CFA, h_1), G) 200 | G_V = np.where(G_m == 0, _cnv_v(CFA, h_0) + _cnv_v(CFA, h_1), G) 201 | 202 | C_H = np.where(R_m == 1, R - G_H, 0) 203 | C_H = np.where(B_m == 1, B - G_H, C_H) 204 | 205 | C_V = np.where(R_m == 1, R - G_V, 0) 206 | C_V = np.where(B_m == 1, B - G_V, C_V) 207 | 208 | D_H = np.abs(C_H - np.pad(C_H, ((0, 0), 209 | (0, 2)), mode=str('reflect'))[:, 2:]) 210 | D_V = np.abs(C_V - np.pad(C_V, ((0, 2), 211 | (0, 0)), mode=str('reflect'))[2:, :]) 212 | 213 | del h_0, h_1, CFA, C_V, C_H 214 | 215 | k = np.array( 216 | [[0, 0, 1, 0, 1], 217 | [0, 0, 0, 1, 0], 218 | [0, 0, 3, 0, 3], 219 | [0, 0, 0, 1, 0], 220 | [0, 0, 1, 0, 1]]) # yapf: disable 221 | 222 | d_H = convolve(D_H, k, mode='constant') 223 | d_V = convolve(D_V, np.transpose(k), mode='constant') 224 | 225 | del D_H, D_V 226 | 227 | mask = d_V >= d_H 228 | G = np.where(mask, G_H, G_V) 229 | M = np.where(mask, 1, 0) 230 | 231 | del d_H, d_V, G_H, G_V 232 | 233 | # Red rows. 234 | R_r = np.transpose(np.any(R_m == 1, axis=1)[np.newaxis]) * np.ones(R.shape) 235 | # Blue rows. 236 | B_r = np.transpose(np.any(B_m == 1, axis=1)[np.newaxis]) * np.ones(B.shape) 237 | 238 | k_b = np.array([0.5, 0, 0.5]) 239 | 240 | R = np.where( 241 | np.logical_and(G_m == 1, R_r == 1), 242 | G + _cnv_h(R, k_b) - _cnv_h(G, k_b), 243 | R, 244 | ) 245 | 246 | R = np.where( 247 | np.logical_and(G_m == 1, B_r == 1) == 1, 248 | G + _cnv_v(R, k_b) - _cnv_v(G, k_b), 249 | R, 250 | ) 251 | 252 | B = np.where( 253 | np.logical_and(G_m == 1, B_r == 1), 254 | G + _cnv_h(B, k_b) - _cnv_h(G, k_b), 255 | B, 256 | ) 257 | 258 | B = np.where( 259 | np.logical_and(G_m == 1, R_r == 1) == 1, 260 | G + _cnv_v(B, k_b) - _cnv_v(G, k_b), 261 | B, 262 | ) 263 | 264 | R = np.where( 265 | np.logical_and(B_r == 1, B_m == 1), 266 | np.where( 267 | M == 1, 268 | B + _cnv_h(R, k_b) - _cnv_h(B, k_b), 269 | B + _cnv_v(R, k_b) - _cnv_v(B, k_b), 270 | ), 271 | R, 272 | ) 273 | 274 | B = np.where( 275 | np.logical_and(R_r == 1, R_m == 1), 276 | np.where( 277 | M == 1, 278 | R + _cnv_h(B, k_b) - _cnv_h(R, k_b), 279 | R + _cnv_v(B, k_b) - _cnv_v(R, k_b), 280 | ), 281 | B, 282 | ) 283 | 284 | # RGB = np.stack([R, G, B]) 285 | 286 | del k_b, R_r, B_r 287 | 288 | if refining_step: 289 | R, G, B = refining_step_Menon2007((R, G, B), (R_m, G_m, B_m), M) 290 | 291 | del M, R_m, G_m, B_m 292 | 293 | return np.stack([R*wb[0], G*wb[1], B*wb[2]], axis=-1) 294 | 295 | demosaicing_DDFAPD = demosaicing_Menon2007 296 | 297 | def refining_step_Menon2007(RGB, RGB_m, M): 298 | """ 299 | Performs the refining step on given *RGB* colourspace array. 300 | Parameters 301 | ---------- 302 | RGB : array_like 303 | *RGB* colourspace array. 304 | RGB_m : array_like 305 | *Bayer* CFA red, green and blue masks. 306 | M : array_like 307 | Estimation for the best directional reconstruction. 308 | Returns 309 | ------- 310 | ndarray 311 | Refined *RGB* colourspace array. 312 | -------- 313 | """ 314 | 315 | R, G, B = RGB 316 | R_m, G_m, B_m = RGB_m 317 | # M = M.astype(np.float32) 318 | 319 | del RGB, RGB_m 320 | 321 | # Updating of the green component. 322 | R_G = R - G 323 | B_G = B - G 324 | 325 | FIR = np.ones(3) / 3 326 | 327 | B_G_m = np.where( 328 | B_m == 1, 329 | np.where(M == 1, _cnv_h(B_G, FIR), _cnv_v(B_G, FIR)), 330 | 0, 331 | ) 332 | R_G_m = np.where( 333 | R_m == 1, 334 | np.where(M == 1, _cnv_h(R_G, FIR), _cnv_v(R_G, FIR)), 335 | 0, 336 | ) 337 | 338 | del B_G, R_G 339 | 340 | G = np.where(R_m == 1, R - R_G_m, G) 341 | G = np.where(B_m == 1, B - B_G_m, G) 342 | 343 | # Updating of the red and blue components in the green locations. 344 | # Red rows. 345 | R_r = np.transpose(np.any(R_m == 1, axis=1)[np.newaxis]) * np.ones(R.shape) 346 | # Red columns. 347 | R_c = np.any(R_m == 1, axis=0)[np.newaxis] * np.ones(R.shape) 348 | # Blue rows. 349 | B_r = np.transpose(np.any(B_m == 1, axis=1)[np.newaxis]) * np.ones(B.shape) 350 | # Blue columns. 351 | B_c = np.any(B_m == 1, axis=0)[np.newaxis] * np.ones(B.shape) 352 | 353 | R_G = R - G 354 | B_G = B - G 355 | 356 | k_b = np.array([0.5, 0, 0.5]) 357 | 358 | R_G_m = np.where( 359 | np.logical_and(G_m == 1, B_r == 1), 360 | _cnv_v(R_G, k_b), 361 | R_G_m, 362 | ) 363 | R = np.where(np.logical_and(G_m == 1, B_r == 1), G + R_G_m, R) 364 | R_G_m = np.where( 365 | np.logical_and(G_m == 1, B_c == 1), 366 | _cnv_h(R_G, k_b), 367 | R_G_m, 368 | ) 369 | R = np.where(np.logical_and(G_m == 1, B_c == 1), G + R_G_m, R) 370 | 371 | del B_r, R_G_m, B_c, R_G 372 | 373 | B_G_m = np.where( 374 | np.logical_and(G_m == 1, R_r == 1), 375 | _cnv_v(B_G, k_b), 376 | B_G_m, 377 | ) 378 | B = np.where(np.logical_and(G_m == 1, R_r == 1), G + B_G_m, B) 379 | B_G_m = np.where( 380 | np.logical_and(G_m == 1, R_c == 1), 381 | _cnv_h(B_G, k_b), 382 | B_G_m, 383 | ) 384 | B = np.where(np.logical_and(G_m == 1, R_c == 1), G + B_G_m, B) 385 | 386 | del B_G_m, R_r, R_c, G_m, B_G 387 | 388 | # Updating of the red (blue) component in the blue (red) locations. 389 | R_B = R - B 390 | R_B_m = np.where( 391 | B_m == 1, 392 | np.where(M == 1, _cnv_h(R_B, FIR), _cnv_v(R_B, FIR)), 393 | 0, 394 | ) 395 | R = np.where(B_m == 1, B + R_B_m, R) 396 | 397 | R_B_m = np.where( 398 | R_m == 1, 399 | np.where(M == 1, _cnv_h(R_B, FIR), _cnv_v(R_B, FIR)), 400 | 0, 401 | ) 402 | B = np.where(R_m == 1, R - R_B_m, B) 403 | 404 | del R_B, R_B_m, R_m 405 | 406 | return R, G, B 407 | 408 | ''' 409 | recovery the RAW image from RGB image 410 | @param pattern: the basis bayer pattern 411 | ''' 412 | def mosaicing(rgb, pattern='rggb'): 413 | pattern = pattern.upper() 414 | dic = {'R': 0, 'G': 1, 'B': 2} 415 | raw = np.zeros(rgb.shape[:2]) 416 | for color, (x, y) in zip(pattern, [(0, 0), (0, 1), (1, 0), (1, 1)]): 417 | raw[x::2, y::2] = rgb[x::2, y::2, dic[color]] 418 | return raw 419 | 420 | if __name__ == '__main__': 421 | from PIL import Image 422 | img = np.array(Image.open('F:\\BinZhang\\Codes\\BurstDenosing\\burst-denoising-master\\dataset\\train\\0001c8fbfb30d3a6.jpg')) 423 | raw = np.zeros(img.shape[:2]) 424 | raw[0::2, 0::2] = img[0::2, 0::2, 0] 425 | raw[0::2, 1::2] = img[0::2, 1::2, 1] 426 | raw[1::2, 0::2] = img[1::2, 0::2, 1] 427 | raw[1::2, 1::2] = img[1::2, 1::2, 2] 428 | rgb = demosaicing_bilinear(raw, 'rggb') 429 | Image.fromarray(raw).show(title='RAW') 430 | Image.fromarray(rgb).show(title='Original') 431 | Image.fromarray(img).show(title='Bilinear') 432 | 433 | --------------------------------------------------------------------------------