VVbuys Blog

standalone Linux lover

本博客记录在VS code中配置LeetCode的过程,在VS Code中可以方便我们调试代码。

一、配置环境

1、安装VSCode并配置环境

Leetcode插件本身是不需要配置编程语言环境的,因为它使用的是LeetCode官方编译器进行调试。

所以正常只需要安装VSCode即可,这一步比较简单。

2、 安装LeetCode插件

在插件扩展中搜索LeetCode插件,安装热度最高的那个即可。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/安装LeetCode.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/安装LeetCode.png)

3、 安装Node.js

在安装完LeetCode插件之后,点击LeetCode插件的选项页,会提示你安装Node.js,按照安装程序的提示安装即可。

Node.js在安装完成之后会自动配置环境变量,但需要记住安装位置,后边配置LeetCode会使用到。

4、登录LeetCode账户

完成上述操作后,建议重启一次VS Code。

(1)修改站点

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/修改站点为中国LeetCode.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/修改站点为中国LeetCode.png)

(2)登录账户

登录账户前请登录网页版LeetCode确认自己的账户名(非昵称)和密码(很可能没有设置)。

然后点击LeetCode插件按钮,点击Sign in LeetCode,然后输入自己的账号和密码,即可登录。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/登录LeetCode.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/登录LeetCode.png)

5、 配置Node.js路径

点击扩展按钮,选中LeetCode插件,鼠标右键选择扩展设置
找到Node Path,选择相应路径。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/配置nodejs路径.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/配置nodejs路径.png)

6、 配置文件路径

编程的代码文件都会保存到本地,默认路径为“$HOME.leetcode”。

我们可以自行设置其保存到我们的项目路径
点击扩展按钮,选中LeetCode插件,鼠标右键选择扩展设置
找到Workspace Folder,输入绝对路径。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/答案文件路径.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/答案文件路径.png)

这样我们就可以在当前路径中找到我们写过的每一个问题的代码:

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/问题代码展示.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/问题代码展示.png)

二、使用操作

1、选择题目

LeetCode插件提供类似网页版的题目筛选功能,可以按照题目序号、难度、标签等筛选。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/选择题目.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/选择题目.png)

2、编辑题目代码

选择一个题目,双击就能出现具体的题目描述。

左侧显示编程窗口,右侧显示题目描述。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/题目描述.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/题目描述.png)

如果代码中出现STL报错,这是因为默认模板中并没有增加STL头文件,

为了方便编程过程中提示,我们可以主动增加头文件。

点击上图中的Submit按钮就可以提交结果,

3、使用测试用例

点击Test,会有三个选项供你选择。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/测试用例.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/测试用例.png)

需要注意的是:在使用第二个自行输入用例时,可能有的用例会有多个输入,需要用到换行符,由于LeetCode插件默认“Enter”键为输入结束,所以输入用例时不能使用“Enter”表示换行,需要我们手动输入“\n“代替换行符。

4、打印输出信息

在代码中添加输出信息,然后使用Test可以输出信息。

![](/img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/使用测试案例输出信息.png)

![](..//img-post/环境配置/2021-11-15-在VS Code中配置LeetCode/使用测试案例输出信息.png)

​ 本博客是作者复现《S3Net: A Single Stream Structure for Depth Guided Image Relighting》的训练代码的笔记。

S3net项目的程序结构

一、搭建网络模型

二、训练网络模型

1、获取数据集dataloader获取模型model获取优化器optimizer获取学习率调整器scheduler

2、使用数据集跑n个epoch

​ (1)跑1个eopch (遍历一遍数据集)

​ 获取x,y

​ 正向传播得到y’(model.forward)

计算损失(get_loss)

​ 反向传播(optimizer.zero_grad,loss.backward,optimizer.step)

​ (2)动态调整学习率(scheduler.step)

​ (3)定期保存模型(torch.load,model.load_state_dict)

​ (4)打印日志到控制台(tqdn进度条技术)

3、保存实验数据到磁盘(MetricRecorder类)

​ (1)保存损失、PSNR、SSIM等到.csv文件

​ (2)保存输入图片、预测图片、目标图片为.png

三、测试网络模型

1、如何加载模型和保存模型

函数:保存模型:

torch.save({‘state_dict’:network.state_dict()}, save_path)是下述代码中最重要的API

1
2
3
4
5
6
7
8
# 保存模型
def save_model(self, save_dir, network, epoch):
# 获取保存文件路径
save_filename = '%s_net.pth' % (epoch)//模型文件名
save_path = os.path.join(save_dir, save_filename)
# 保存网络模型
torch.save({'state_dict':network.state_dict()}, save_path)

用以上save_model函数定期保存模型:

技巧:在每个epoch保存模型时,同时保存latest模型,万一中断训练,方便加载模型、继续训练。

1
2
3
4
5
6
7
8
# 定期保存模型
# if self.metric_recorder.update_best_model('PSNR'):
# self.model.save(self.option.model_path, 'best')
if epoch % self.option.save_freq == 0 and epoch != 0:
self.save_model(self.option.model_path,self.model, 'latest')
self.save_model(self.option.model_path,self.model, epoch)
np.savetxt(self.iter_path, (epoch, self.n_total_iter), delimiter=',', fmt='%d')
print('成功保存模型:epoch %d, iters %d' % (epoch, self.n_total_iter))

函数:加载模型:

torch.load(save_path)和model.load_state_dict(checkpoint[‘state_dict’])是下述代码中最重要的API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 导入网络模型
from network.res2net2 import Dehaze3
# 获取模型
def get_model(self):
# 加载网络模型
model = Dehaze3().to(self.option.device)
# 是否加载预训练好的模型
if self.option.is_pretrain_model:
# 得到保存模型的路径
save_path = os.path.join(self.option.model_path, 'latest_net.pth')
# 加载之前保存好的模型
checkpoint = torch.load(save_path)
self.start_epoch, self.n_total_iter = np.loadtxt(self.iter_path, delimiter=',', dtype=int)//iter.txt保存之前训练保存的最后的模型的epoch和iter
model.load_state_dict(checkpoint['state_dict'])
print('成功预加载网络模型!')
else:
self.start_epoch = 0
self.n_total_iter = 0 # 训练的总迭代次数
print('成功创建网络模型!')
return model

2、如何输出实验数据到.csv

MetricRecorder类:用于记录数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MetricRecorder():
# 把实验数据添加到scalarDict字典中
def add_current_scalar(self, log_dict:dict):
for tag, value in log_dict.items():
self.scalarDict[tag].append(value)
#把scalarDict字典中的每条实验数据添加到csv文件中
def _write_to_csv(self,epoch_num,validation):
if self.save_to_csv:
csv_name = 'val_result.csv' if validation else 'train_result.csv'//得到csv文件的名字
self.csv_helper.save_one_epoch(epoch_num, log_dict=self.scalarDict,csv_name=csv_name)
# self.save_to_csv为True,则调用 self._write_to_csv()把实验数据添加到csv中
def write_one_epoch(self, epoch_num, validation=False):
if self.use_tb_log:
self._write_to_tensorboard(epoch_num,validation)
if self.save_to_csv:
self._write_to_csv(epoch_num,validation)
if self.save_to_png:
self._write_to_png(epoch_num)

使用MetricRecorder类得到数据,并保存数据:

初始化MetricRecorder类

1
2
3
4
5
6
7
8
9
10
11
12
# save_to_csv=True,  # 是否保存到.csv确认把实验数据保存到csv文件中
class Trainer():
def __init__(self,option:argparse.Namespace):
# 初始化数据记录器
self.metric_recorder = MetricRecorder(self.option.output_path,
use_tb_log=False, # 是否使用tb日志
save_to_csv=True, # 是否保存到.csv
save_to_png=True, # 是否保存到.png
csv_name=None,
write_header=self.option.is_pretrain_model
) # 实验描述

得到数据字典logDict,用self.metric_recorder.add_current_scalar函数获取到数据字典logDict,使MetricRecorder类里的函数_write_to_csv能使用logDict数据。

1
2
3
4
5
logDict = {'loss': losses['loss'].item(), "loss_chaL1": losses['loss_chaL1'].item(),
"loss_wssim": losses['loss_wssim'].item(),"loss_pre": losses['loss_pre'].item(),
"PSNR": curr_psnr, "SSIM": curr_ssim}
self.metric_recorder.add_current_scalar(logDict)

调用metric_recorder.write_one_epoch,保存每个回合的数据:

1
2
# 保存该回合的数据
self.metric_recorder.write_one_epoch(epoch, validation=False)

3、如何保存预测图片

调用metric_recorder.add_current_imgs获取图片名称和图片的字典,使metric_recorder里的_write_to_png函数能使用图片,并保存

1
2
3
4
5
# 输出图片
if i == epoch_size-1:# 当i是最后一个批次时保存图片
# 添加本回合的生成的图片
imgDict = {'ori_image': ori_image, 'guide_image': guide_image, 'pre_image': pre_image,'truth_img': truth_img}
self.metric_recorder.add_current_imgs(imgDict) # 记录图片

调用metric_recorder.write_one_epoch,保存每个回合的数据:

1
2
3

# 保存该回合的数据
self.metric_recorder.write_one_epoch(epoch, validation=False)

4、如何使用进度条功能

​ 本博客是作者复现《S3Net: A Single Stream Structure for Depth Guided Image Relighting》的训练数据集读取代码的笔记。

一、函数test_trainSet()

函数功能:测试类trainDataSetFromTrack2的功能。

1、给出输入原始图像的路径和引导图像路径

1
2
origin_img_path = '../datasets/alltrain/*.png'# 输入的原始图像的路径
guide_img_path = origin_img_path # 引导图像路径

2、根据图片路径和想获取的图片数量获取数据集。

1
dataset = trainDataSetFromTrack2(origin_img_path, guide_img_path,10)

3、用DataLoader获取可以输入神经网络中的数据集

1
trainloader = DataLoader(dataset, batch_size=1, shuffle=True, num_workers=0)

4、得到一组样本图片,iter函数将可序列化的对象序列化,next按顺序取序列化后对象的数据。

1
batchdict = next(iter(trainloader))

5、获取原始图像及其深度图、引导图像及其深度图。

1
ori_image, guide_image, ori_depth, guide_depth = batchdict['x']

6、将图片保存到对象路径中

1
save_img(ori_image,'./1.png')

函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_trainSet():
# 创建数据集
origin_img_path = '../datasets/alltrain/*.png'# 输入的原始图像的路径
guide_img_path = origin_img_path # 引导图像路径
dataset = trainDataSetFromTrack2(origin_img_path, guide_img_path,10)# 根据图片路径读取数据集
trainloader = DataLoader(dataset, batch_size=1, shuffle=True, num_workers=0)
# 输出信息
print("训练集一共有{}/{}={}个的批次,其中{}是mini-batch".format(len(dataset), 1, len(trainloader), 1))
batchdict = next(iter(trainloader))# 得到一组样本数据
ori_image, guide_image, ori_depth, guide_depth = batchdict['x']
img_name = batchdict['img_name']
print(ori_image.shape)
print(guide_image.shape)
print(ori_depth.shape)
print(guide_depth.shape)
print('img_name', img_name)
save_img(ori_image,'./1.png')

二、类trainDataSetFromTrack2

类trainDataSetFromTrack2的功能:实现加载数据集所需的各个函数。

1、类头

该类继承自类Dataset,需要重载函数__init__()、getitem(self, index)、len(self)(这三个函数开头结尾都有两个下划线,typora文档里没显示出来)。

1
class trainDataSetFromTrack2(Dataset):

2、成员函数__init__()

函数代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def __init__(self,
origin_img_path: str, # 输入文件所在的路径
guide_img_path: str, # 输出文件所在的路径
num:int,# 读取的图片数量
):
super(trainDataSetFromTrack2, self).__init__()
# 获取所有图片的路径
self.origin_img_paths, self.guide_img_paths = self._get_dataset_path(origin_img_path, guide_img_path)
self.len = len(self.origin_img_paths)
# 选取指定数量的图片
if num > 0 and num < self.len:
self.origin_img_paths = self.origin_img_paths[:num]
self.guide_img_paths =self.guide_img_paths[:num]
self.len = num
# 获取图像预处理函数
self.preprocess_fn = data_transform
print(f'含有{self.len} 个样本的数据集已被创建')

函数功能

1、获取所有输入的原始图像和引导图像的路径

1
self.origin_img_paths, self.guide_img_paths = self._get_dataset_path(origin_img_path, guide_img_path)

2、获取读取整个数据集的大小

1
self.len = len(self.origin_img_paths)

3、获取指定数量的图片

1
2
3
4
if num > 0 and num < self.len:
self.origin_img_paths = self.origin_img_paths[:num]
self.guide_img_paths =self.guide_img_paths[:num]
self.len = num

4、获取图像预处理函数

1
self.preprocess_fn = data_transform

3、成员函数__getitem__()

函数代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获取一组图片数据
def __getitem__(self, index):
# 获取一组样本的路径
origin_img_path, guide_img_path = self.origin_img_paths[index % self.len], self.guide_img_paths[index % self.len]
origin_depth_name = origin_img_path.split('_')[0]+'.npy' # 拼接出原始图像对应深度图的路径:Image000+.npy
guide_depth_name = guide_img_path.split('_')[0]+'.npy' # 拼接出指导图像对应深度图的路径: Image001+.npy
truth_img_name = origin_img_path.split('_')[0]+'_'+guide_img_path.split('_')[1]+'_'+guide_img_path.split('_')[2]# 拼接出真实图像的路径:原始图像的前缀Image000+指导图像的后缀
# 读取该组样本的RGB图片
ori_image, guide_image,truth_img = map(self._read_rgb_img, (origin_img_path, guide_img_path,truth_img_name))

# 读取该组样本的depth图片
ori_depth, guide_depth = map(self._read_depth_img, (origin_depth_name, guide_depth_name))
# 获取该组样本对应的名称
img_name = origin_img_path.split('\\')[1]
return {'x':(ori_image, guide_image, ori_depth, guide_depth),
'y':truth_img,
'img_name':img_name}

函数功能:根据序号index,获取一组样本图片。

1、获取原始图像及其深度图、引导图像及其深度图、真实图像的路径

1
2
3
4
5
6
7
8
# 根据序号index,获取原始图像、引导图像的路径
origin_img_path, guide_img_path = self.origin_img_paths[index % self.len], self.guide_img_paths[index % self.len]
# 拼接出原始图像对应深度图的路径:Image000+.npy
origin_depth_name = origin_img_path.split('_')[0]+'.npy'
# 拼接出指导图像对应深度图的路径: Image001+.npy
guide_depth_name = guide_img_path.split('_')[0]+'.npy'
# 拼接出真实图像的路径:原始图像的前缀Image000+指导图像的后缀
truth_img_name = origin_img_path.split('_')[0]+'_'+guide_img_path.split('_')[1]+'_'+guide_img_path.split('_')[2]

2、# 读取该组样本的RGB图片

1
ori_image, guide_image,truth_img = map(self._read_rgb_img, (origin_img_path, guide_img_path,truth_img_name))

map()相当于调用了函数self._read_rgb_img三次,以上代码还可以写为

1
2
3
ori_image = self._read_rgb_img(origin_img_path)
guide_image = self._read_rgb_img(guide_img_path)
truth_img = self._read_rgb_img(truth_img_name)

3、读取该组样本的depth图片

1
ori_depth, guide_depth = map(self._read_depth_img, (origin_depth_name, guide_depth_name))

4、返回读取的这组样本图片

1
2
3
return {'x':(ori_image, guide_image, ori_depth, guide_depth),
'y':truth_img,
'img_name':img_name}

4、成员函数 __len__()

函数功能:返回读取图片的数量。

函数代码:

1
2
def __len__(self):
return self.len

5、成员函数_read_rgb_img()

类中的成员函数加上一个下划线_,这样类外就不能访问该函数。

函数功能:根据给定的图片路径,获取图片张量。

函数代码:

1
2
3
4
5
def _read_rgb_img(self,img_path):
img = Image.open(str(img_path)) # (1024,1024,4)
image_tensor = self.preprocess_fn(img) # tensor,size=(4,1024,1024)
image_tensor = image_tensor[:3, :, :] # tensor,size=(3,1024,1024)
return image_tensor

6、成员函数_read_depth_img()

函数功能:根据给定的图片路径,获取深度图片张量。

函数代码:

1
2
3
4
5
6
def _read_depth_img(self,depth_path):
depth = np.load(depth_path, allow_pickle=True).item()['normalized_depth']
ori_depth = torch.unsqueeze(torch.from_numpy(depth), 0) # 升维(1,1024,1024)
#ori_depth = torch.unsqueeze(ori_depth, 0) # 升维(1,1,1024,1024)
return ori_depth

7、成员函数_get_dataset_path()

函数功能:根据给定的图片文件夹的路径,获取图片文件夹中所有图片的路径。

glob.glob函数:搜索所有满足条件的项。

函数代码:

1
2
3
4
5
6
def _get_dataset_path(self, input_file_path, target_file_path):
origin_img_paths = sorted(glob.glob(input_file_path, recursive=True))
guide_img_paths = glob.glob(target_file_path, recursive=True)
random.shuffle(guide_img_paths)
#assert len(origin_img_paths) == len(guide_img_paths)
return origin_img_paths, guide_img_paths

三、数据增强手段

代码

1
2
3
data_transform = transforms.Compose([
transforms.ToTensor(),
])

四、函数save_img()

函数功能:把图片张量tensor_img保存到输出文件夹output_dir中。

函数代码:

1
2
3
def save_img(tensor_img,output_dir):
# 保存图像
torchvision.utils.save_image(tensor_img, output_dir)

《S3Net: A Single Stream Structure for Depth Guided Image Relighting》是来自中国台湾的Hao-Hsiang Yang等人发表在CVPR 2021(CCF推荐的A类会议)上的一篇WorkShip论文,本文是其项目训练代码的关键复现流程。

​ 本博客是复现《S3Net: A Single Stream Structure for Depth Guided Image Relighting》的损失函数实现。该项目的S3Net一共使用了三个损失函数,其整体损失如下:
$$
L_{\text {Total }}=\lambda_{1} L_{\text {cha }}+\lambda_{2} L_{W-S S I M}+\lambda_{3} L_{P e r}
$$
​ 其中 $\lambda_{1}$、$\lambda_{2}$ 和 $\lambda_{3}$ 是缩放系数,用于调整三个分量的相对权重。

一、Charbonnier 损失

​ 该损失函数来自于《A general and adaptive robust loss function》,其可以看做是一个高鲁棒性的L1损失函数,该损失函数可以还原全局结构并且可以更鲁棒地处理异常值,其公式如下:
$$
L_{C h a}(I, \hat{I})=\frac{1}{T} \sum_{i}^{T} \sqrt{\left(I_{i}-\hat{I}_{i}\right)^{2}+\epsilon^{2}}
$$
​ 其中$I$ 和$\hat{I}$ 分别代表目标图像和该文网络输出的预测图像, $\epsilon$被视为一个微小的常数(例如$10^{-6}$​),用来实现稳定和鲁棒的收敛。根据这篇超分辨领域的论文《Fast and Accurate Image Super-Resolution with Deep Laplacian Pyramid Networks》,采用该函数可以使得模型的收敛速度加快。其实现代码相对简单“

1
2
3
4
5
6
7
8
9
10
11
class L1_Charbonnier_loss(torch.nn.Module):
"""L1 Charbonnierloss."""
def __init__(self):
super(L1_Charbonnier_loss, self).__init__()
self.eps = 1e-6

def forward(self, X, Y):
diff = torch.add(X, -Y)
error = torch.sqrt(diff * diff + self.eps)
loss = torch.mean(error)
return loss

二、SSIM 损失

​ 该损失函数来自于《Loss functions for image restoration with neural networks》 ,其能够重建局部纹理和细节。 可以表示为:
$$
L_{S S I M}(I, \hat{I})=-\frac{\left(2 \mu_{I} \mu_{\hat{I}}+C_{1}\right)\left(2 \sigma_{I \hat{I}}+C_{2}\right)}{\left(\mu_{I}^{2}+\mu_{\hat{I}}^{2}+C_{1}\right)\left(\sigma_{I}^{2}+\sigma_{\hat{I}}^{2}+C_{2}\right)}
$$
​ 其中 σ 和 µ 表示图像的标准偏差、协方差和均值。

​ 在图像重照明任务中,为了从原始图像中去除阴影,该文扩展了 SSIM 损失函数,以便使网络可以恢复更详细的部分。

​ 该文使用《Y-net: Multiscale feature aggregation network with wavelet structure similarity loss function for single image dehazing》 中的方法将 DWT 组合到 SSIM 损失中,这有利于重建重光照图像的清晰细节。最初,DWT 将预测图像分解为四个不同的小sub-band图像。 操作可以表示为:
$$
\hat{I}^{L L}, \hat{I}^{L H}, \hat{I}^{H L}, \hat{I}^{H H}=\operatorname{DWT}(\hat{I})
$$

​ 其中上标表示来自各个过滤器的输出(例如,$$\hat{I}^{L L}, \hat{I}^{L H}, \hat{I}^{H L}, \hat{I}^{H H}$$)。

​ $$\hat{I}^{H L}, \hat{I}^{L H}, \hat{I}^{H H}$$分别是水平边缘、垂直边缘和角点检测的高通滤波器。 fLL 被视为下采样操作。 此外,DWT 可以不断分解$$\hat{I}^{L L}$$ 以生成具有不同尺度和频率信息的图像。 这一步写成:
$$
\hat{I}{i+1}^{L L}, \hat{I}{i+1}^{L H}, \hat{I}{i+1}^{H L}, \hat{I}{i+1}^{H H}=\operatorname{DWT}\left(\hat{I}{i}^{L L}\right)
$$
​ 其中下标 i 表示第 i 次 DWT 迭代的输出。 上述 SSIM 损失项是根据原始图像对和各种子带图像对计算得出的。 SSIM损失和DWT的融合整合为:
$$
\begin{array}{l}
L
{W-S S I M}(I, \hat{I})=\sum_{0}^{r} \gamma_{i} L_{\mathrm{SSIM}}\left(I_{i}^{w}, \hat{I}{i}^{w}\right) \
w \in{L L, H L, L H, H H}
\end{array}
$$
其中$\gamma
{i}$​ 基于原文来控制不同补丁的重要性。

​ 这里的实现我们参考了wavelet_ssim的实现。

三、感知损失

​ 该损失函数来自于2016年代ECCV会议的《Perceptual losses for real-time style transfer and super-resolution》,该论文在图像转换问题中使用感知损失(perceptual loss)函数代替之前的逐像素(per-pixel)损失函数,结果在速度和图片质量上均得到了大幅度提升。

​ 感知损失定义为
$$
L_{P e r}(I, \hat{I})=\mid(\operatorname{VGG}(I)-\operatorname{VGG}(\hat{I}) \mid
$$

​ 其中$\mid·\mid$ 是绝对值。

​ 该损失函数利用从预训练的深度神经网络(例如 VGG19 (《Very deep convolutional networks for large-scale image recognition》))中获得的多尺度特征,然后使用L1损失来测量预测图像和目标图像之间的视觉特征差异,从而使得训练的图像尽可能地逼近目标图像。该项目使用在ImageNet 上预训练的 VGG19 被用作损失函数网络。

​ 首先使用代码获取vgg19模型:

1
2
3
4
5
import torch
import torchvision.models as models
# 加载预训练的模型
vgg_model = models.vgg19(pretrained=True)
print(vgg_model.features)

​ vgg19整体结构分为’features’, ‘avgpool’, 和 ‘classifier’三大部分,而计算损失函数只需要用到’features’部分,打印其结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace)
(16): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(17): ReLU(inplace)
(18): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(19): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace)
(23): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(24): ReLU(inplace)
(25): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(26): ReLU(inplace)
(27): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace)
(30): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(31): ReLU(inplace)
(32): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(33): ReLU(inplace)
(34): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(35): ReLU(inplace)
(36): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)

这里我们使用[DRN项目中的vggloss](DeepRelight/networks.py at master · WangLiwen1994/DeepRelight (github.com))的实现来获取多尺度特征,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Vgg19(nn.Module):
def __init__(self, requires_grad=False):
super(Vgg19, self).__init__()
# vgg_pretrained_features = models.vgg19(pretrained=True).features # #pretrained是true,导入预训练模型
# 加载预训练模型
vgg19_model = models.vgg19(pretrained=True)
# 获取中间层特征
vgg_pretrained_features = vgg19_model.features

self.slice1 = torch.nn.Sequential()
self.slice2 = torch.nn.Sequential()
self.slice3 = torch.nn.Sequential()
self.slice4 = torch.nn.Sequential()
self.slice5 = torch.nn.Sequential()
# 获取中间层特征,把不同层的特征分别加入不同的模块
for x in range(2):
self.slice1.add_module(str(x), vgg_pretrained_features[x])
for x in range(2, 7):
self.slice2.add_module(str(x), vgg_pretrained_features[x])
for x in range(7, 12):
self.slice3.add_module(str(x), vgg_pretrained_features[x])
for x in range(12, 21):
self.slice4.add_module(str(x), vgg_pretrained_features[x])
for x in range(21, 28):
self.slice5.add_module(str(x), vgg_pretrained_features[x])
# 设置所有参数都不需要计算梯度,使得之后不进行反向传播及权重更新
if not requires_grad:
for param in self.parameters():
param.requires_grad = False

def forward(self, X):

h_relu1 = self.slice1(X)
h_relu2 = self.slice2(h_relu1)
h_relu3 = self.slice3(h_relu2)
#h_relu4 = self.slice4(h_relu3)
#h_relu5 = self.slice5(h_relu4)
#out = [h_relu1, h_relu2, h_relu3, h_relu4, h_relu5]
out = [h_relu1, h_relu2, h_relu3]#, h_relu4, h_relu5]
return out
class VGGLoss(nn.Module):
def __init__(self, gpu_ids):
super(VGGLoss, self).__init__()
self.vgg = Vgg19().cuda()
self.criterion = nn.L1Loss() # L1损失,平均绝对值损失
self.weights = [1.0 / 32, 1.0 / 16, 1.0 / 8, 1.0 / 4, 1.0]

def forward(self, x, y):
x_vgg, y_vgg = self.vgg(x), self.vgg(y)
loss = 0.0
for i in range(len(x_vgg)):
loss += self.weights[i] * self.criterion(x_vgg[i], y_vgg[i].detach())
return loss

​ 《Deep Relighting Networks for Image Light Source Manipulation》是发表在ECCV 2020上的一篇论文。这里是原文链接原文代码

摘要

​ 操纵给定图像的光源的现有的方法通常需要额外的信息,如场景的几何结构,这可能不适用于大多数图像。在本文中,我们用公式表示单图像重照明任务,并提出了一种新的深度重照明网络(DRN),该网络由三部分组成:

1)场景重建,其目的是通过深度自动编码网络显示主要场景结构,

2)阴影先验估计,通过对抗性学习,从新的灯光方向预先确定灯光效果,

3)重新渲染,将主要结构与重建的阴影视图结合起来,形成目标光源下所需的估计图像。

​ 实验结果表明,该方法在定性和定量上都优于其他方法。具体而言,提出的DRN在2020年ECCV大会的“AIM2020-任何对一重新照明挑战”中实现了最佳峰值信噪比。

一、简介

​ 图像是这个信息时代流行的信息载体,直观易懂。 显示设备的快速发展刺激了人们对高质量画面的需求。 图像的视觉外观与照明高度相关,这在摄影和电影摄影等各种应用中至关重要。 不适当的照明通常会导致各种视觉退化问题,例如不想要的阴影和扭曲的颜色。 然而,光源(如太阳光)难以控制,或者有时无法改变(对于捕获的图像),因此很难生成令人满意的图像。 在拍摄的图像上产生想要的光源效果的方法已经是非常引人关注的高科技方法,因为它可以修改拍摄图像的光照。

​ 已经提出了一些方法,旨在减轻由不适当照明引起的退化。

方法 论文 主要内容
直方图均衡化 histogram equalization (HE) [37]《Contrast limited adaptive histogram equalization based enhancement for real time video system》 重新排列强度以服从均匀分布,这可以增加对低对比度区域的识别。 它操纵全局光条件,平衡了整个图像的照明。
高动态范围 (HDR) 领域中的方法 [3] 《Recovering high dynamic range radiance maps from photographs》、[36]《Deep high dynamic range imaging 》 通过增加低对比度区域的动态范围来提高图像质量。HDR 方法可以看作是局部对比度的细化,但缺乏对全局光的调整。
基于 Retinex 的方法 [29]《Retinex processing for auto-matic image enhancement》、[35]《Deep retinex decomposition for low -light enhancement》 将图像分离为光照和反射率的组合,其中反射存储场景的固有内容,在不同的照明条件下无法改变。 通过细化光照,可以提高图像的视觉质量。
低光图像增强方法 [15]《 Enlightengan: Deep light enhancement without paired supervision》【32】《Lightening network for low-light image enhancement》 改善黑暗环境的可见度,以照亮整个画面。
阴影去除 【13】《Direction-aware spatial context features for shadow detection》、【18】《Shadow removal via shadow image decomposition》 旨在消除光源造成的阴影效果,但不能模拟目标光源的阴影。

​ 调整光源为基于照明的图像增强提供了一种灵活而自然的方式。 尽管已经进行了大量的研究来改进照明,但从操纵光源的角度进行研究的效果较小。 也就是说,通过控制光源来改变光照效果还处于设想阶段。 重新照明领域的文献主要关注特定应用,

例如

人像重新照明 人像重新照明相关论文
人像重新照明:这些方法需要在一般场景中无法实现的先验信息(如面部标志、几何先验)。 【26】《Learning physics-guided face relighting under directional light》【31】《Single image portrait relighting》【40】《Deep single-image portrait relighting》

​ 卷积神经网络(CNN)最近因其强大的学习能力而备受关注。 它可以在强大的计算资源的支持下消化大量的训练数据并 提取有识别力的表征 。CNN 在各种任务中显示出显着的优势,例如

图像分类 [17,30]、语义分割 [27,38]、超分辨率 [8,23]、位置识别 [1,19] 等。 由于浅层的参数往往面临梯度消失和爆炸的风险,因此难以训练。残差学习 《 Deep residual learning for image recognition》 通过在每个处理块之间添加shortcut连接来减轻优化难度。 在归一化层的帮助下,梯度可以稳定地从深层流向浅层,极大地提高了深度网络的训练效率。 更深的结构通常意味着更多可训练的参数,因此可以带来更强大的学习能力,这使得可以处理更具挑战性的任务,例如单幅图像重新照明。

​ 本文中的图像重光照方法侧重于使用强大的深度 CNN 架构来操纵光源的位置和色温。 它不仅可以调整主色调,还可以重新投射给定图像的阴影。 如图 1 所示,我们专注于特定的“any-to-one”重新照明任务,其输入:在任意光源(任意方向或色温,见图 1(a))下的凸显,目标是 估计特定光源下的图像(方向:E,色温:4500K,见图1(b))。 所提出的方法可以推广到其他与光相关的任务。

image-20211104233058382

本文方法的创新点:

1、我们不是将输入图像直接映射到目标光照条件,而是将重新照明任务分为三个部分:场景重建、光照效果估计和重新渲染过程。

2、 为了保留下采样和上采样过程的更多信息,我们将反投影理论插入到自动编码器结构中,这有利于场景重建和光照效果估计。

3、 光照效果难以衡量,增加了训练难度。 我们使用对抗性学习策略:新的阴影区域鉴别器,为训练过程提供指导。

二、相关工作

Back-Projection (BP) theory

相关工作 相关论文
关于低光图像增强的工作 [32] Lightening network for low-light image enhancement
BP理论在单图像超分辨率领域很流行[9,20,21]。基于 BP 的方法不是直接学习从输入到目标的映射,而是迭代地消化残差并改进估计。 它更加关注学习过程中出现的弱点(即残差),这显着提高了深度 CNN 架构的效率。 【9】《Deep back-projection networks for super-resolution》、【20】《 Hierarchical back projection network for image super-resolution》、【21】《Image super-resolution via attention based back projection networks》。

​ 关于低光图像增强的工作 [32] 将 BP 理论扩展到光域传输任务。 它假设低光 (LL) 和正常光 (NL) 图像分别位于 LL 和 NL 域。 首先,一个lightening算子从 LL 输入预测 NL 估计。 然后,darkening算子将 NL 估计映射回 LL 域(LL 估计)。 在 LL 域中,可以找到 LL 输入和 LL 估计之间的差异(LL 残差),这表明两个转移算子(变亮和变暗lightening and darkening)的弱点。 之后,LL 残差通过另一个lightening算子映射回 NL 域(NL 残差)。 NL 残差然后细化 NL 估计以获得更好的输出。 从数学上讲,enlightening过程可以写为:

image-20211104234138291

​ 其中 L 和 ˆN ∈ RH×W×3 分别表示 LL 输入图像和 NL 估计。 H、W 和 3分别代表高度、宽度和 RGB 通道。 符号 L1 和 L2 是两个lightening算子,分别照亮 LL 图像和 LL 残差。 符号 D 是将 NL 估计映射到 LL 域的Darkening算子。两个加权系数 λ1 和 λ2 ∈ R 用于平衡残差计算和最终细化。

Adversarial Learning

​ 将图像转换为相应的输出图像通常形成为像素级回归任务,其损失函数(如 L1 或 L2 范数损失)表示所有像素的平均误差。 这种损失函数忽略了像素之间的相互关系,容易扭曲感知结构,导致输出模糊。 大量的研究工作已经完成了图像之间感知相似性的定量测量,如结构相似性(SSIM)[34]《Image quality assessment: from error visibility to structural similarity》、学习感知图像块相似性(LPIPS)[39]《The unreasonable effffectiveness of deep features as a perceptual metric》、Gram矩阵[6]《A neural algorithm of artistic style》等。 然而,感知评估基本上因不同的视觉任务而异,难以制定。

​ 生成对抗网络 (GAN) [7,14,25] 【7】《Generative adversarial nets》【14】《Image-to-image translation with conditional adversarial networks》【25】《 Conditional generative adversarial nets》提供了一种新颖的解决方案,将感知测量嵌入到对抗学习的过程中。 每个 GAN 由一个生成器和一个鉴别器组成。鉴别器旨在在目标图像中找到潜在的感知结构,然后指导生成器的训练。 随后,生成器提供次优估计,作为鉴别器训练过程的负样本。 对于分组的负和正(目标图像)样本,判别器执行二元分类任务,测量两类样本之间的潜在感知差异。 整个训练过程是

image-20211105180808841

​ 其中 D 和 G 分别表示鉴别器和生成器。 项 X 和 Y 分别代表输入和目标图像。 在训练过程中,生成器和判别器进行两人的极小极大博弈。 鉴别器学习区分估计图像 G(X) 和目标图像 Y。生成器旨在最小化估计 G(X) 和目标图像 Y 之间的差异。训练过程遵循对抗性学习策略, 越来越多地学习和使用目标图像内的潜在分布。 最后,训练将达到动态平衡,其中生成器产生的估计具有与真实目标图像相似的潜在感知结构。

三、方法

​ 如图 2 所示,所提出的深度重新照明网络 (DRN) 由三部分组成:场景重建、阴影先验估计和重新渲染器。 首先,输入图像在场景重建网络(见第 3.2 节)中处理以去除照明的影响,这从输入图像中提取固有结构。 同时,另一个分支(阴影先验估计,见3.3节)侧重于光照效果的变化,根据目标光源重新投射阴影。 接下来,重新渲染器部分(参见第 3.4 节)感知光照效果并在结构信息的支持下重新绘制图像。 场景重建和阴影先验估计网络都具有类似的深度自动编码器结构,这是一种Pix2Pix网络增强的变体。三个组件的细节展示如下:

3.1 Assumption of Relighting

​ 《S3Net: A Single Stream Structure for Depth Guided Image Relighting》是来自中国台湾的Hao-Hsiang Yang等人发表在CVPR 2021(CCF推荐的A类会议)上的一篇WorkShip论文,这里是原文链接原文代码

摘要

​ 深度引导(Depth guided)的任意到任意( any-to-any)图像重光照的目的是通过原始图像和相应的深度图生成重照明的图像,来匹配给定引导图像及其深度图的照明设置。 据该文所称,这项任务是一个在以前的文献中没有提及过的新挑战。

​ 为了解决这个问题,该文提出了一种基于深度学习的单流结构的神经网络,称为S3Net。 该网络是一个编码器-解码器( encoder-decoder)模型,其输入是 原始图像、引导图像和相应的深度图,共计4张图(2张RGB图+2张深度图)。 该网络的特点是向解码器部分中加入了注意力模块和增强模块,用来关注引导图像中与重照明相关的区域。

​ 最终的实验表明,该论文提出的模型在竞赛(the NTIRE 2021 Depth Guided Any-to-any Relighting Challenge)中实现了第三高的SSIM。

一、简介

​ 图像重照明是一项新兴且关键的技术,其在可视化、图像编辑和增强现实 (AR) 中的具有较大应用潜力,例如为第一人称和第三人称游戏渲染具有各种环境照明条件的图像。该文的目的是解决深度引导的any to any的重光照任务,该任务的特点是用引导图像的照明设置来重新照明输入图像。这里给出一组图像说明:

![](\img-post\论文分享\2021-11-04-s3net:深度引导图像重照明的单流结构\any_to_any图像.png)

1、any-to-any重光照任务和风格转换的异同点:

任务类型 相同点 不同点
风格迁移 输入是原始图像和引导图像 风格迁移一般侧重于纹理渲染
(any to any)重光照 输入是原始图像和引导图像(及其深度图) 需要去除原始图像的阴影,并且在预测图像中生成新的阴影,风格转换一般做不到这点

2、any-to-any重光照任务的相关研究

​ 因为深度卷积神经网络 (CNN) 在许多计算机视觉任务中取得了成功,而且之前的重光照方法都直接使用CNN并遵循端到端(end-to-end)的方式直接生成重光照图像(没有假定任何物理先验),受这些方法的启发,该文依旧使用深度学习网络来解决深度引导的任意对任意重照明任务。

作者年份 论文题目 主要贡献
Puthussery和Kuriakose等人,2020 WDRN: A wavelet decomposed relightnet for image relighting 2
Hu和Huang等人 ,ECCV,2020 SA-AE for any-to-any relighting 3
Guo和Liao等人,BMVC,2019 Deep learning fusion of rgb and depth images for pedestrian detection 4
Xu和Sunkavalli等人, ToG,2018 Deep image-based relighting from optimal sparse samples 5
Yang和Chen等人,CVPRW,2021 Multi modal bifurcated network for depth guided image relighting 6

​ 与传统的图像重光照任务不同,该文使用了NTIRE 2021竞赛中提供的额外的深度图,这有利于模型学习场景的物理空间表示。

​ 该文在解码器部分使用了多尺度特征提取器和注意力机制,多尺度特征提取器 可用于增加感受野并整合粗到细的表示,有必要采用这个模块,因为重光照图像包含各种尺度的对象;注意力机制可以分配特征图权重来放大局部区域的特征,由于重光照图像包含方向信息,使用注意力机制有利于模型学习方向的特征表示。

​ 该文在 VIDIT 数据集上测试了我们提出的方法,多个实验表明,所提出的 S3Net 在 NTIRE 2021 深度引导任意对任意重新照明挑战中实现了第三高的 SSIM 和 MPS。

​ 笔者认为该文的创新点或贡献如下:

1、提出了一个单流结构网络 (S3Net)处理any to any重光照任务

2、为了设计一个高效的图像重照明网络,在解码器部分加入了注意力模块和增强模块

3、为了进一步优化模型,该文在目标函数上使用了结合了离散小波变换(DWT)理论的多尺度损失函数,提升了准确性。

二、相关工作

1、带有深度图的图像处理

​ 与传统的只使用RGB图像的计算机视觉任务不同,使用额外的深度信息可以提高许多计算机视觉任务的准确性,当与 RGB 图像结合使用时,深度图已被证明是提供几何和空间信息的有用提示。

​ 2019年Guo和Liao等人的《Deep learning fusion of rgb and depth images for pedestrian detection》,提出了 Faster RCNN模型来解决行人检测问题,该工作证明可以利用深度图来细化从 RGB 图像中提取的卷积特征, 而且可以在深度信息的帮助下探索透视投影,从而实现更准确的区域建议。

​ 2020年Chen和Lin等人的ECCV上的《Bi-directional cross-modality feature propagation with separation-and-aggregation gate for rgb-d semantic segmentation》中,提出了一种统一且高效的跨模态引导的语义分割编码器。 这种结构在跨模态聚合之前联合过滤和重新校准两种表示。 同时,引入了双向多步传播策略以有效融合两种模态之间的信息。

​ 为了有效地提取 RGB 图像和深度特征,最流行的方法是使用双流主干网络的结构,如下相关研究:

作者年份 论文题目 主要贡献
Pang和Zhang等人,2020 Hierarchical dynamic filtering network for rgb-d salient object detection 使用双流主干,输入为RGB图像和深度图
Chen和Fu等人 ,ECCV,2020 Progressively guided alternate refinement network for rgb-d salient object detection 使用双流主干,输入为RGB图像和深度图

​ 然后,VIDIT 数据集中重光照图像的大小和相应的深度图非常大(1024×1024),且每次输入都是两个RGB图像和两个深度图,所以这种输入会在双主干网络结构中造成巨大的计算负担,而且论文作者认为重光照图像包含光源方向信息,这不适合在训练期间将大图像裁剪为小块, 所以该文设计了一个单一的流结构来联合提取深度和图像特征。

2、基于深度学习的图像重光照

​ 在NTIRE 2021的竞赛规则中,有两种重光照任务设置:

作者年份 论文题目 相关研究
one-to-one重光照 光源方向和光源色温是预先定义的 《WDRN: A wavelet decomposed relightnet for image relighting》、《Multi modal bifurcated network for depth guided image relighting》
any-to-any重光照 光源方向和光源色温是基于一张引导图像 《SA-AE for any-to-any relighting》

​ 由于都是图像到图像的转换任务,重光照任务似乎与其他低级计算机视觉任务非常相似,例如图像去雾 、图像烟雾去除、图像去雪、反射去除和水下图像增强等,但是与它们不同的是,重光照任务包含光源方向和色温的信息,需要在预测图像中估计物体的阴影。

​ 图像到图像的转换任务经常使用编码器-解码器架构,其中U-net [22, 23] 是最流行的图像到图像转换任务网络:

作者年份 论文题目 主要贡献
Ronneberger和Fischer等人,2015 《U-net: Convolutional networks for biomedical image segmentation》 提出了用于生物医学图像语义分割的卷积神经网络:Unet
Hu和Shen等人,CVPR,2018 《Squeeze-and-excitation networks》 提出了通道注意力模块(CAM)

​ Unet结构不仅包括编码器-解码器结构,还包括跳跃连接,它将从编码器到解码器具有相同大小的特征连接起来。 例如,Puthussery 等人 [2] 提出了一种用于图像重光照的离散小波分解重光照网络。

3、离散小波变换(Wavelet Transform)

​ 1999年Elsevier等人的论文《A wavelet tour of signal processing》提出了离散小波变换(DWT),该操作可以将图像分解为不同频率间隔的各种小块,可以替代现有的下采样操作,如最大池化或平均池化。 因此,许多计算机视觉任务应用 DWT 来减少特征图并实现多尺度特征,如下:

​ 2019年ICIP上的《Wavelet U-net and the chromatic adaptation transform for single image dehazing》;

​ 2020年ECCV上的《WDRN: A wavelet decomposed relightnet for image relighting》 。

​ 2020年的CASSP上的《Y-net: Multiscale feature aggregation network with wavelet structure similarity loss function for single image dehazing》利用 DWT 来设计目标函数来测量真实图像和预测图像之间的相似性。受该工作的启发,该文也在损失函数中结合了 DWT,使其网络可以学习多尺度表示。

4、注意力机制

​ 注意机制在人类感知系统和深度学习任务中都起着重要作用。注意机制提供特征图或某些序列权重,以便可以放大区域或位置的特征。 具体来说,对于计算机视觉任务,注意力机制分为空间注意力和通道注意力。 前者在空间上利用权重来细化特征图,后者计算全局平均池化特征以实现通道注意力。 在本文中,我们的模型利用了这两种注意力机制来进一步提高网络的性能。

作者年份 论文题目 主要贡献
Mnih和Heess 等人,2014 《Recurrent models of visual attention》 空间注意力
Hu和Shen等人,CVPR,2018 《Squeeze-and-excitation networks》 通道注意力
Yang等人,ECCV,2020 《Wavelet channel attention module with a fusion network for single image deraining》 通道注意力,注意力机制应用于图像增强领域
Hu和Huang等人 ,ECCV,2020 《SA-AE for any-to-any relighting》 空间注意力

三、方法

1、网络模型

​ 该文的整个网络模型的输入是原始RGB图(1024x1024x3)、原始深度图(1024x1024x1)、引导RGB图(1024x1024x3)和引导深度图(1024x1024x1)连接在一起形成的8通道张量(1024x1024x8),输出的是3通道的预测RGB图(1024x1024x3)。

​ 本文提出的 S3Net 的架构如下图所示。该网络基于《Knowledge transfer dehazing network for nonhomogeneous dehazing》,包含编码器和解码器部分。

![](\img-post\论文分享\2021-11-04-s3net:深度引导图像重照明的单流结构\S3net整体架构.png)

​ 【编码器】该文使用《Res2net: A new multi-scale backbone architecture》提出的Res2Net101网络主干作为编码器,因为Res2Net 可以在粒度级别表示多尺度特征,并增加每个网络层的感受野范围,输入通过主干后可以实现多尺度特征提取。该文的工作在Res2Net做了如下修改:

  • 修改第一个卷积使网络可以使用8 通道张量作为输入;
  • 丢弃网络最后的全连接层,使最终输出的特征图的大小为 16分之一 ;
  • 编码器的初始权重是用 ImageNet 训练的预训练参数,底部特征使用跳跃连接连接到解码器。

​ 【解码器】解码器由卷积堆栈组成,以细化特征图。

​ 利用注意力模块(Attention,P)来细化中间特征。 注意模块由残差层 (residual layer)《Deep residual learning for image recognition》、空间注意力模块(SAM)《Recurrent models of visual attention》 和通道注意力模块(CAM)《Squeeze-and-excitation networks》 组成。

​ 利用像素混洗(Pixel shuffle,P)《Real-time single image and video super-resolution using an efficient subpixel convolutional neural network》和转置卷积(Transposed convolution,T)《Pixel transposed convolutional networks》来放大特征图。

​ 此外,受《Enhanced pix2pix dehazing network》的启发,该文章在 S3Net 中添加了增强模块。 增强模块利用不同步幅的平均池化来改变特征图和感受野的大小,这对于提取多尺度特征是有效的。 最后,应用上采样来恢复减少的特征图,并将所有特征图拼接起来。

​ 【跳跃连接】众所周知,类 U-Net 结构在许多任务中是有益的,例如图像去雾(《PMS-net: Robust haze removal based on patch map for single images》,《PMHLD: patch map-based hybrid learning dehazenet for single image haze removal》) 和语义分割 (《U-net: Convolutional networks for biomedical image segmentation》)。 它的跳跃连接鼓励特征重用。 因此S3Net 中也采用跳跃连接将来自主干的最后三个特征图合并到它们对应的特征图。

2、损失函数

​ 该文章的S3Net一共使用了三个损失函数,其整体损失如下:
$$
L_{\text {Total }}=\lambda_{1} L_{\text {cha }}+\lambda_{2} L_{W-S S I M}+\lambda_{3} L_{P e r}
$$
​ 其中 $\lambda_{1}$、$$\lambda_{2}$ 和$ $\lambda_{3}$ 是缩放系数,用于调整三个分量的相对权重。

(1)Charbonnier 损失

​ 该损失函数来自于《A general and adaptive robust loss function》,其可以看做是一个高鲁棒性的L1损失函数,该损失函数可以还原全局结构并且可以更鲁棒地处理异常值,其公式如下:
$$
L_{C h a}(I, \hat{I})=\frac{1}{T} \sum_{i}^{T} \sqrt{\left(I_{i}-\hat{I}_{i}\right)^{2}+\epsilon^{2}}
$$
​ 其中$I$ 和$\hat{I}$ 分别代表目标图像和该文网络输出的预测图像, $\epsilon$被视为一个微小的常数(例如$10^{-6}$),用来实现稳定和鲁棒的收敛。

(2)SSIM 损失

​ 该损失函数来自于《Loss functions for image restoration with neural networks》 ,其能够重建局部纹理和细节。 可以表示为:
$$
L_{S S I M}(I, \hat{I})=-\frac{\left(2 \mu_{I} \mu_{\hat{I}}+C_{1}\right)\left(2 \sigma_{I \hat{I}}+C_{2}\right)}{\left(\mu_{I}^{2}+\mu_{\hat{I}}^{2}+C_{1}\right)\left(\sigma_{I}^{2}+\sigma_{\hat{I}}^{2}+C_{2}\right)}
$$
​ 其中 σ 和 µ 表示图像的标准偏差、协方差和均值。

​ 在图像重照明任务中,为了从原始图像中去除阴影,该文扩展了 SSIM 损失函数,以便使网络可以恢复更详细的部分。

​ 该文使用《Y-net: Multiscale feature aggregation network with wavelet structure similarity loss function for single image dehazing》 中的方法将 DWT 组合到 SSIM 损失中,这有利于重建重光照图像的清晰细节。最初,DWT 将预测图像分解为四个不同的小sub-band图像。 操作可以表示为:
$$
\hat{I}^{L L}, \hat{I}^{L H}, \hat{I}^{H L}, \hat{I}^{H H}=\operatorname{DWT}(\hat{I})
$$

​ 其中上标表示来自各个过滤器的输出(例如,$$\hat{I}^{L L}, \hat{I}^{L H}, \hat{I}^{H L}, \hat{I}^{H H}$$)。

​ $$\hat{I}^{H L}, \hat{I}^{L H}, \hat{I}^{H H}$$分别是水平边缘、垂直边缘和角点检测的高通滤波器。 fLL 被视为下采样操作。 此外,DWT 可以不断分解$$\hat{I}^{L L}$$ 以生成具有不同尺度和频率信息的图像。 这一步写成:
$$
\hat{I}{i+1}^{L L}, \hat{I}{i+1}^{L H}, \hat{I}{i+1}^{H L}, \hat{I}{i+1}^{H H}=\operatorname{DWT}\left(\hat{I}{i}^{L L}\right)
$$
​ 其中下标 i 表示第 i 次 DWT 迭代的输出。 上述 SSIM 损失项是根据原始图像对和各种子带图像对计算得出的。 SSIM损失和DWT的融合整合为:
$$
\begin{array}{l}
L
{W-S S I M}(I, \hat{I})=\sum_{0}^{r} \gamma_{i} L_{\mathrm{SSIM}}\left(I_{i}^{w}, \hat{I}{i}^{w}\right) \
w \in{L L, H L, L H, H H}
\end{array}
$$
其中$$\gamma
{i}$$ 基于原文来控制不同补丁的重要性。

(3)感知损失

​ 该损失函数来自于《Perceptual losses for real-time style transfer and super-resolution》, 与前面提到的两个损失函数不同,感知损失利用从预训练的深度神经网络(例如 VGG19 (《Very deep convolutional networks for large-scale image recognition》))获得的多尺度特征来测量预测图像和目标图像之间的视觉特征差异。该文章使用在ImageNet 上预训练的 VGG19 被用作损失函数网络。

​ 感知损失定义为
$$
L_{P e r}(I, \hat{I})=\mid(\operatorname{VGG}(I)-\operatorname{VGG}(\hat{I}) \mid
$$

​ 其中$\mid·\mid$ 是绝对值。

四、实验

     该文的实验使用的是NTIRE 2021中使用的数据集是the Virtual Image Dataset for Illumination Transfer (VIDIT) 。 该图像数据集总共有15600张图片,包含390 个不同的虚拟场景及其对应的390 张深度图,每个场景具有40种不同的照明设置(从2500到6500K的五种不同色温和 8 个方位角)。 所有训练图像和深度图的大小分别为 1024 × 1024 × 3 和 1024 × 1024 × 1。  

​ 在整个数据集中300 个场景用于训练,剩下的 90 个场景用于验证和测试。

作者年份 论文题目 主要贡献
Helou和Zhou等人,2020 《Vidit: Virtual image dataset for illumination transfer》 提出了用于图像重光照的虚拟图像数据集VIDIT

1、消融实验的定量分析

![](\img-post\论文分享\2021-11-04-s3net:深度引导图像重照明的单流结构\消融实验的定量分析.png)

2、消融实验的定性分析

![](\img-post\论文分享\2021-11-04-s3net:深度引导图像重照明的单流结构\消融实验的定性分析.png)

3、与其他方法的对比实验

​ 如下表所示,列出的是本文提出的方法和在NTIRE2021 workshop的深度引导的any-to-any重光照挑战中的其他竞争方法的对比结构:

![](\img-post\论文分享\2021-11-04-s3net:深度引导图像重照明的单流结构\对比实验.png)

​ 结果显示本文方法的结果在 SSIM、PSNR、LPIPS 和 MPS 方面分别获得了第 3、第 4、第 2 和第 3 名。
​ 本文模型在测试阶段平均需要 2.042 秒来生成 1024 × 1024大小的 重光照图像。

五、总结与不足

​ 该文提出了一种单流结构网络(S3Net)用于深度引导的任意对任意的图像重照明。该网络的编码器部分基于Res2Net,解码器部分加入了注意力模块和增强模块 ;损失函数中加入了离散小波变换的SSIM损失。

​ 该方法在NTIRE 2021的PMS 和 SSIM 方面获得了第三名,但是实验证明,该文的方法也会在某些条件下可能会失败,如下图所示,当原始图像包含大面积的阴影时,该文的模型无法识别它们的前景和背面,导致预测图像与真实图像非常不同,论文作者认为这是因为即使给出了深度图,这些信息也只是提供了正面而不是全方位的空间信息。

![](\img-post\论文分享\2021-11-04-s3net:深度引导图像重照明的单流结构\模型的失败案例.png)

感知损失

项目DRN和S3Net都是用的 J. Johnson, A. Alahi, and L. Fei-Fei, 2016《Perceptual losses for real-time style transfer and super-resolution,” in European conference on computer vision》的感知函数

感知损失利用从预训练的深度神经网络(例如 VGG19 )中获得的多尺度特征来测量真实图像和网络预测图像之间的视觉特征差异。 在DRN、S3Net中使用 ImageNet训练集 上预训练的 VGG19 网络来计算感知损失。

​ 感知损失(使用VGG)公式为:

Vgg19网络结构:

VGG19包含了19个隐藏层(16个卷积层和3个全连接层)。

img

DRN代码中,使用Pytorch获取Vgg19网络:

1
2
3
4
5
6
7
import torch
import torchvision.models as models
# 加载预训练的模型
vgg_model = models.vgg19(pretrained=True)
# 获取中间层特征
vgg_pretrained_features = vgg_model.features
print('model.features[0]', model.features[0]) #Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from torchvision import models

class Vgg19(torch.nn.Module):
def __init__(self, requires_grad=False):
super(Vgg19, self).__init__()
# 加载预训练的模型
# vgg_pretrained_features = models.vgg19(pretrained=True).features

# 加载预训练模型
model = models.vgg19(pretrained=True)
# 获取中间层特征
vgg_pretrained_features = model.features

self.slice1 = torch.nn.Sequential()
self.slice2 = torch.nn.Sequential()
self.slice3 = torch.nn.Sequential()
self.slice4 = torch.nn.Sequential()
self.slice5 = torch.nn.Sequential()
# 把不同层的特征分别加入不同的模块
for x in range(2):
self.slice1.add_module(str(x), vgg_pretrained_features[x])
for x in range(2, 7):
self.slice2.add_module(str(x), vgg_pretrained_features[x])
for x in range(7, 12):
self.slice3.add_module(str(x), vgg_pretrained_features[x])
for x in range(12, 21):
self.slice4.add_module(str(x), vgg_pretrained_features[x])
for x in range(21, 28):
self.slice5.add_module(str(x), vgg_pretrained_features[x])
# 设置所有参数都不需要计算梯度
if not requires_grad:
for param in self.parameters():
param.requires_grad = False

def forward(self, X):
# 获取不同模块的特征
h_relu1 = self.slice1(X)
h_relu2 = self.slice2(h_relu1)
h_relu3 = self.slice3(h_relu2)
#h_relu4 = self.slice4(h_relu3)
#h_relu5 = self.slice5(h_relu4)
#out = [h_relu1, h_relu2, h_relu3, h_relu4, h_relu5]
out = [h_relu1, h_relu2, h_relu3]#, h_relu4, h_relu5]
return out

用vgg计算感知损失(VGGLoss)

VGG19本是用来进行分类的,进行可视化和用作VGG loss 自然也就是用到全连接层之前的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
class VGGLoss(nn.Module):
def __init__(self, gpu_ids):
super(VGGLoss, self).__init__()
self.vgg = Vgg19().cuda()
self.criterion = nn.L1Loss()
self.weights = [1.0 / 32, 1.0 / 16, 1.0 / 8, 1.0 / 4, 1.0]

def forward(self, x, y):
x_vgg, y_vgg = self.vgg(x), self.vgg(y)
loss = 0
for i in range(len(x_vgg)):
loss += self.weights[i] * self.criterion(x_vgg[i], y_vgg[i].detach())
return loss

Charbonnier loss

Charbonnier 损失 ( A general and adaptive robust loss function ),可以看作是鲁棒的 L1 损失函数。

其中 I 和 I^ 分别代表真实图像和来自提出网络的 relit 图像,并且 e 被视为一个微小的常数(例如 10−6 )以实现稳定和鲁棒的收敛。 LC ha 可以还原全局结构并且可以更鲁棒地处理异常值。

image-20211103001556088

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'''''''''
@文件名: ChaLoss.py
@作者: XW
@时间: 2021/11/2 23:05
@环境: Python,Numpy
@描述: 无
@参考: 无
'''''''''
import torch
import numpy as np

#定义L1损失函数
def cha_loss(y_true,y_pre):

e = 10**(-6)
loss=torch.mean(torch.sqrt((y_true-y_pre)**2)+e**2)
# loss = torch.sqrt((y_true-y_pre)**2)
# loss = np.mean(np.sqrt((y_true - y_pre) ** 2) )
return loss

if __name__=='__main__':
y_true=torch.tensor([1.0,2.0,3.0])
y_pre=torch.tensor([4.0,5.0,6.0])
res=cha_loss(y_true,y_pre)
print(type(res))

参考链接: C++中sort函数使用方法 - 俊宝贝 - 博客园 (cnblogs.com)

1.sort函数包含在头文件为#include的c++标准库中,调用标准库里的排序方法可以实现对数据的排序,但是sort函数是如何实现的,我们不用考虑!

2.sort函数的模板有三个参数:

1
void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

(1)第一个参数first:是要排序的数组的起始地址。

(2)第二个参数last:是结束的地址(最后一个数据的后一个数据的地址)

(3)第三个参数comp是排序的方法:可以是从升序也可是降序。如果第三个参数不写,则默认的排序方法是从小到大排序。

3 实例

1
2
3
4
5
6
7
8
9
10
11
12

1 #include<iostream>
2 #include<algorithm>
3 using namespace std;
4 main()
5 {
6   //sort函数第三个参数采用默认从小到大
7   int a[]={45,12,34,77,90,11,2,4,5,55};
8   sort(a,a+10);
9   for(int i=0;i<10;i++)
10   cout<<a[i]<<" ";
11 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

1 #include<iostream>
2 #include<algorithm>
3 using namespace std;
4 bool cmp(int a,int b);
5 main(){
6   //sort函数第三个参数自己定义,实现从大到小
7   int a[]={45,12,34,77,90,11,2,4,5,55};
8   sort(a,a+10,cmp);
9   for(int i=0;i<10;i++)
10     cout<<a[i]<<" ";
11 }
12 //自定义函数
13 bool cmp(int a,int b){
14   return a>b;
15 }

LeetCode题目 相关题目类型 相关链接
383 赎金信(简单难度) 383. 赎金信 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26]={0};
// 记录magazine里各个字符出现的次数
for(int i=0;i<magazine.length();i++){
record[magazine[i]-'a']++;
}
// 遍历ransomNote,在record里对应的字符个数做--操作
for(int j=0;j<ransomNote.length();j++){
record[magazine[i]-'a']--;
// 如果小于零,说明ransomNote里出现的字符,magazine里没有
if(record[magazine[i]-'a']<0){
return false;
}
}
return true;


}
};

类似题目:

LeetCode题目 相关题目类型 相关链接
242 有效的字母异位词(简单难度) 242. 有效的字母异位词 - 力扣(LeetCode) (leetcode-cn.com)

数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。

定义一个数组叫做record用来上记录字符串s里字符出现的次数。

1、需要把字符映射到数组也就是哈希表的索引下表上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下表0,相应的字符z映射为下表25。

再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。

2、那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

3、那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。

最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。

时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
};

知识补充:

1、c++ memset()函数

memset 函数是内存赋值函数,用来给某一块内存空间进行赋值的;

包含在<string.h>头文件中,可以用它对一片内存空间逐字节进行初始化;

原型为 :

void *memset(void *s, int v, size_t n);

这里s可以是数组名,也可以是指向某一内在空间的指针;

v为要填充的值;

n为要填充的字节数;

示例1:

1
2
3
4
5
6
7
8
9
10
struct data
{
char num[100];
char name[100];
int n;
};
struct data a, b[10];

memset( &a, 0, sizeof(a) ); //注意第一个参数是指针类型,a不是指针变量,要加&
memset( b, 0, sizeof(b) ); //b是数组名,就是指针类型,不需要加&

示例2:

1
2
3
4
5
6
7
char str[9];

//我们用memset给str初始化为“00000000”,用法如下

memset(str,0,8);

//注意,memset是逐字节 拷贝的。

示例3:

1
2
3
4
5
6
7
8
9
10
11
int num[8];

//我们用memset给str初始化为{1,1,1,1,1,1,1,1},
memset(num,1,8);//这样是不对的

//一个int是4个字节的,8个int是32个字节,所以首先要赋值的长度就不应该为8而是32。

//因为memset是 逐字节 拷贝,以num为首地址的8字节空间都被赋值为1,

//即一个int变为0X00000001 00000001 00000001 00000001,显然,把这个数化为十进制不会等于1的

2、 常见的 string 类构造函数有以下几种形式:

strings(num, c) //生成一个字符串,包含num个c字符

LeetCode题目 相关题目类型 相关链接
1002 查找常用字符(简单难度) 1002. 查找共用字符 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
vector<string> commonChars(vector<string>& words) {
vector<string> result;
if (words.size() == 0) return result;
int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率
for (int i = 0; i < words[0].size(); i++) { // 用第一个字符串给hash初始化
hash[words[0][i] - 'a']++;
}

int hashOtherStr[26] = {0}; // 统计除第一个字符串外字符的出现频率
for (int i = 1; i < words.size(); i++) {
memset(hashOtherStr, 0, 26 * sizeof(int));
for (int j = 0; j < words[i].size(); j++) {
hashOtherStr[words[i][j] - 'a']++;
}
// 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数
for (int k = 0; k < 26; k++) {
hash[k] = min(hash[k], hashOtherStr[k]);
}
}
// 将hash统计的字符次数,转成输出形式
for (int i = 0; i < 26; i++) {
while (hash[i] != 0) { // 注意这里是while,多个重复的字符
string s(1, i + 'a'); // char -> string
result.push_back(s);
hash[i]--;
}
}

return result;

}
};

知识补充:

哈希数据结构:unordered_set

参考链接C++ STL unordered_set容器完全攻略 (biancheng.net)

注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果是去除了重复的元素, 同时可以不考虑输出结果的顺序

那么用数组来做哈希表也是不错的选择,例如242. 有效的字母异位词

但是要注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小。

而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构:

  • std::set
  • std::multiset
  • std::unordered_set

std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。

c++ unordered_set容器的成员方法:

find(key): 查找以值为 key 的元素,如果找到,则返回一个指向该元素的正向迭代器;反之,则返回一个指向容器中最后一个元素之后位置的迭代器(如果 end() 方法返回的迭代器)。

思路如图所示:

image-20211030230213881

LeetCode题目 相关题目类型 相关链接
349 两个数组的交集(简单难度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 下面的代码表示:发现nums2的元素 在nums_set里又出现过
//不理解的话,复习unordered_set的成员方法
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};

拓展

那有同学可能问了,遇到哈希问题我直接都用set不就得了,用什么数组啊。

直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。

不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。

LeetCode题目 相关题目类型 相关链接
350 两个数组的交集2(简单难度) 350. 两个数组的交集 II - 力扣(LeetCode) (leetcode-cn.com)

这道题和349不一样,没有要求 输出不能重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
// 对两个数组进行排序
sort(nums1.begin(),nums1.end());
sort(nums2.begin(),nums2.end());
int i=0,j=0;
vector<int> res;
//同时对两个数组遍历,有一个遍历完就结束
while(i<nums1.size() && j<nums2.size()){
// 相等,则添加到结果里
if(nums1[i]==nums2[j]){

res.push_back(nums1[i]);
++i;
++j;
}
else if(nums1[i]<nums2[j])
++i;
else
++j;
}
return res;
}
};
LeetCode题目 相关题目类型 相关链接
202 快乐数(简单难度) 202. 快乐数 - 力扣(LeetCode) (leetcode-cn.com)

题目中说了会无限循环,即在求和的过程中,sum会重复出现,这一点对解题很重要。

正如:关于哈希表,你该了解这些!中所说,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。

这道题用哈希法,来判断这个sum是否重复出现,重复则return false, 否则一直找到sum为1为止。

判断sum是否重复出现就可以使用unordered_set。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
LeetCode题目 相关题目类型 相关链接
1 (简单难度) 1. 两数之和 - 力扣(LeetCode)

则要使用map,那么来看一下使用数组和set来做哈希法的局限。

  • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。

  • set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。

    此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下表。

    C++中map,有三种类型:

    映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
    std::map 红黑树 key有序 key不可重复 key不可修改 O(logn) O(logn)
    std::multimap 红黑树 key有序 key可重复 key不可修改 O(logn) O(logn)
    std::unordered_map 哈希表 key无序 key不可重复 key不可修改 O(1) O(1)

    std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。

    同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。 更多哈希表的理论知识请看关于哈希表,你该了解这些!

    这道题目中并不需要key有序,选择std::unordered_map 效率更高!

    解题思路:

    例如:nums=[2,7,11,15],target=9

    遍历数组

    1、寻找target-nums[i]是否在map中,

    2、没有,9-2=7,7不在map中,把2和对应下标0放进map中

    3、有,9-7=2,2在map中,找到了数值2和7相加等于9,返回其下标

    遍历结束后,没有返回值,则返回空的数组

    参考链接: C++ STL unordered_map容器用法详解 (biancheng.net)

    C++代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Solution {
    public:
    vector<int> twoSum(vector<int>& nums, int target) {
    std::unordered_map <int,int> map;//创建空的umap容器
    for(int i = 0; i < nums.size(); i++) {
    //查找target - nums[i]是否在map中,
    auto iter = map.find(target - nums[i]);
    // 有,返回两个数的下标
    if(iter != map.end()) {
    return {iter->second, i};
    }
    // 没有,把该数值和其下标加入到map中
    map.insert(pair<int, int>(nums[i], i));
    }
    //没找到
    return {};
    }
    };
LeetCode题目 相关题目类型 相关链接
49 (中等难度) 49. 字母异位词分组 - 力扣(LeetCode) (leetcode-cn.com)

参考链接:https://blog.csdn.net/fuqiuai/article/details/83589593

解题思路:

1、对字符串每个词的字母按照字典序进行排序,异位词对字母进行排序后有相同的字母序列。

用排序后的字母序列作为map的key,将所有异位词都保存到字符串数组中,用map建立key和字符串数组之间的映射(不需要结果有序,所以用unordered map)

2、最后把归纳好的字符串数组存入结果res中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> res;
if(strs.size()==0)return res;
// 键为string字符串类型,值为字符串数组类型
unordered_map<string,vector<string>> map1;
for(int i=0;i<strs.size();i++){
string s=strs[i];
sort(s.begin(),s.end());
// 排序后相同的字符串放到一个数组里,键为排序后的字符串
map1[s].push_back(strs[i]);//map1[s]是一个值类型为字符串的数组
}
for(auto v:map1){
res.push_back(v.second);
}
return res;
}
};
0%