语雀:https://www.yuque.com/lart/blog/vw6pcx
随着智能手机的发展,手机自带的相机功能也逐渐变得丰富起来。其中的“人像模式”可以模仿单反相机的效果,将特定目标突出并将其余背景虚化。这一效果可以获得良好的视觉效果,有效的突出感兴趣目标的主体。
从理论上来说,实现这一效果有两种思路。
本文尝试对第二种方法进行一些简单的尝试。
需要说明的是,这里为了方便利用并行加速,所以代码都是基于 pytorch 实现的。
对于第二种思路,一种直观的实现方式是使得模糊核与深度相关联,越远的位置对应的模糊程度越大。
这里我们基于均值滤波的形式进行扩展。让滤波器的权重分布在均值滤波器的基础上进行调整。
由于我们的 depth 图像中深度数据是 0~255 的数值,为了后面将其用作用于调整均值核和恒等核之间的变换参数,这里我们对 depth 图像进行 minmax 归一化:
depth = depth - depth.min()
depth = depth / depth.max()
在我们的处理中,可以注意到与传统的直接对整个背景整体模糊的形式最大的不同,即我们是根据不同位置自身对应的深度信息来自适应的调整模糊程度。所以这种位置自适应的处理形式就需要我们借助于 pytorch 提供的 img2col
函数 unfold
来实现了:
image = image.permute(2, 0, 1).unsqueeze(0)
image = F.pad(image, pad=(kernel_size//2, kernel_size//2, kernel_size//2, kernel_size//2), mode='replicate')
image = F.unfold(image, kernel_size=kernel_size)
image = image.reshape(num_channels, kernel_size*kernel_size, height, width)
这里同时利用 replicate
的模式来补齐边缘处的像素,以确保后期加权平均不会出现明显的暗边。
关于 unfold 的理解,可以参看我之前的文章中的介绍:https://www.yuque.com/lart/papers/frxyq3#sptFi。其就是用于将局部方形邻域的数据收集起来,堆叠到通道维度上。可以说其实现了卷积中的局部聚合操作中加权之前的行为。
weight = get_log_kernel2d_pt(kernel_size=kernel_size, valid_ratio=depth, normalize=True)
这里将归一化后的 depth 传入了 valid_ratio
上。首先对这一参数进行约束,保证数值范围满足要求:
assert valid_ratio.min() >= 0 and valid_ratio.max() <= 1
这一参数用来调整核参数的形式:
基于后面的设计,我们还需要将这一参数映射到 [ 0 , inf ] [0, \inf] [0,inf] 区间上,其中 0 对应于恒等核,而正无穷对应于均值滤波器。这里基于负对数函数的形式:
valid_ratio = -torch.log(1 - valid_ratio)
为了对这些滤波器以均值滤波器为基础进行统一表示,我们引入了绝对值函数作为加权窗口中各点的权重分布形式。即 F.relu(1 - torch.abs(coords / valid_width)).nan_to_num(nan=1)
所表示的形式:
valid_width=0
的时候,对于相对坐标为 0 处,1 - torch.abs(coords / valid_width)
结果为 nan
,而其余位置均为 -inf
,通过 relu
和 .nan_to_num()
处理后,可以得到恒等核,仅保留中心元素,其余元素的参数均为 0。valid_width
趋向于无穷大的时候,此时开始从恒等核逐步向均值核变化。title: PyTorch 中几个特殊值的除法
- inf / inf = nan
- num / inf = 0
- inf / num = inf
- \* / 0 = inf
- nan / \* = nan
- \* / nan = nan
>>> a = torch.tensor([-torch.inf, -1, 0, 1, torch.inf, torch.nan])
>>> a / -torch.inf
tensor([nan, 0., -0., -0., nan, nan])
>>> a / -1
tensor([inf, 1., -0., -1., -inf, nan])
>>> a / 0
tensor([-inf, -inf, nan, inf, inf, nan])
>>> a / 1
tensor([-inf, -1., 0., 1., inf, nan])
>>> a / torch.inf
tensor([nan, -0., 0., 0., nan, nan])
>>> a / torch.nan
tensor([nan, nan, nan, nan, nan, nan])
二维权重可以通过一维权重矩阵相乘来获得。这里利用 einsum
来保留原始的形状,避免直接压缩维度后还需重新调整形状的麻烦。
kernel_1d = F.relu(1 - torch.abs(coords / valid_width)).nan_to_num(nan=1)
kernel_2d = torch.einsum("xhw,yhw->xyhw", kernel_1d, kernel_1d)
kernel_2d = kernel_2d.reshape(-1, *ori_shape) # 压缩前两个维度
最终,我们可以根据需要来对权重进行归一化,以确保总和为 1:
if normalize:
kernel_2d = kernel_2d / kernel_2d.sum(dim=0)
直接对权重和原图相乘加和后得到结果:
tgt_image = (image * weight).sum(dim=1)
tgt_image = tgt_image.permute(1, 2, 0).numpy()
原始RGB图像:
对应的Depth图像:
合成后的虚化图像示例: