语义分割

可以用来做背景虚化。

语义分割将图片中的每个像素分类到对应的类别,语义分割一般是有监督的。

最终用不同像素的颜色代表不同的标号

import os
import torch
import torchvision
from d2l import torch as d2l


# 读取所有VOC图像并标注,VOC是一个数据集格式,
def read_voc_images(voc_dir, is_train=True):
    # 指定哪个是训练集train.txt,哪个是验证集val.txt
    txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation', 'train.txt' if is_train else 'val.txt')
    # 设置彩色通道
    mode = torchvision.io.image.ImageReadMode.RGB
    with open(txt_fname, 'r') as f:
        images = f.read().split()
    features, labels = [], []
    for i, fname in enumerate(images):
        # 读取源文件,用来训练的features
        features.append(torchvision.io.read_image(os.path.join(voc_dir, 'JPEGImages', f'{fname}.jpg')))
        # 对于像素的label,在SegmentationClass文件夹下,存成没有压缩像素点的PNG文件
        labels.append(torchvision.io.read_image(os.path.join(voc_dir, 'SegmentationClass', f'{fname}.png'), mode))
    return features, labels


# @save
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
                [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
                [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
                [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
                [0, 64, 128]]


# 构建RGB到VOC类别索引,把三个元素值map乘整数
def voc_colormap2label():
    # 开辟tensor数组
    colormap2label = torch.zeros(256 ** 3, dtype=torch.long)
    for i, colormap in enumerate(VOC_COLORMAP):
        colormap2label[(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i
    return colormap2label


# 将VOC标签中的RGB值映射到它们的类别索引,第一个参数是图片,第二个参数是字典
def voc_label_indices(colormap, colormap2label):
    # 将RGB放到最后一个维度
    colormap = colormap.permute(1, 2, 0).numpy().astype('int32')
    # 将RGB换成下标
    idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + colormap[:, :, 2])
    # 得到每个像素对应的标号
    return colormap2label[idx]


# 图像增光,随机特征和标签图像
def voc_rand_crop(feature, label, height, width):
    # random是随机裁剪
    rect = torchvision.transforms.RandomCrop.get_params(feature, (height, width))
    # 如果对图片进行裁剪,响应的标号也要做裁剪
    feature = torchvision.transforms.functional.crop(feature, *rect)
    # 这里要固定住随机因子
    label = torchvision.transforms.functional.crop(label, *rect)
    return feature, label


# 加载语义分割数据集
d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar',
                           '4e443f8a2eca6b1dac8a6c57641b67dd40621a49')
voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')
train_features, train_labels = read_voc_images(voc_dir, True)

# @save
VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
               'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
               'diningtable', 'dog', 'horse', 'motorbike', 'person',
               'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']


class VOCSegDataset(torch.utils.data.Dataset):
    """一个用于加载VOC数据集的自定义数据集"""

    def __init__(self, is_train, crop_size, voc_dir):
        self.transform = torchvision.transforms.Normalize(
            mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
        self.crop_size = crop_size
        features, labels = read_voc_images(voc_dir, is_train=is_train)
        # 对俩者都进行feature
        self.features = [self.normalize_image(feature)
                         for feature in self.filter(features)]
        self.labels = self.filter(labels)
        self.colormap2label = voc_colormap2label()
        print('read ' + str(len(self.features)) + ' examples')

    # 调用上面定义的ransform值进行归一化处理
    def normalize_image(self, img):
        return self.transform(img.float() / 255)

    # 如果图像更小的话就不要了
    def filter(self, imgs):
        return [img for img in imgs if (
                img.shape[1] >= self.crop_size[0] and
                img.shape[2] >= self.crop_size[1])]

    def __getitem__(self, idx):
        feature, label = voc_rand_crop(self.features[idx], self.labels[idx],
                                       *self.crop_size)
        return (feature, voc_label_indices(label, self.colormap2label))

    def __len__(self):
        return len(self.features)


def load_data_voc(batch_size, crop_size):
    """加载VOC语义分割数据集"""
    voc_dir = d2l.download_extract('voc2012', os.path.join(
        'VOCdevkit', 'VOC2012'))
    num_workers = d2l.get_dataloader_workers()
    train_iter = torch.utils.data.DataLoader(
        VOCSegDataset(True, crop_size, voc_dir), batch_size,
        shuffle=True, drop_last=True, num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(
        VOCSegDataset(False, crop_size, voc_dir), batch_size,
        drop_last=True, num_workers=num_workers)
    return train_iter, test_iter


y = voc_label_indices(train_labels[0], voc_colormap2label())
# 画出一个区域
print(y[105:115, 130:140], VOC_CLASSES[1])
n = 5
imgs = []
for _ in range(n):
    imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)
imgs = [img.permute(1, 2, 0) for img in imgs]
d2l.show_images(imgs[::2] + imgs[1::2], 2, n)
d2l.plt.show()
crop_size = (320, 480)
# 大小为1114,语义分割数据集标起来很贵
voc_train = VOCSegDataset(True, crop_size, voc_dir)
voc_test = VOCSegDataset(False, crop_size, voc_dir)

batch_size = 64
train_iter = torch.utils.data.DataLoader(voc_train, batch_size, shuffle=True,
                                         drop_last=True,
                                         num_workers=d2l.get_dataloader_workers())

for X, Y in train_iter:
    print(X.shape)
    print(Y.shape)
    break

load_data_voc(voc_train, voc_test)

转置卷积

卷积不会增大输入的高宽,通常要么不变、要么减半

转置卷积可以用来增大输入高宽,类似卷积的逆运算

$$ Y[i:i+h,j:j+w]+=X[i,j]\cdot K $$

为什么称之为转置

对于卷积$Y=X\star W$

可以对W构造一个V,使得卷积等价于矩阵乘法$Y^{\prime}=VX^{\prime}$

这里Y',X'是Y,X对应的向量版本

转置卷积等价于$Y^\prime=V^TX^\prime $

如果卷积将输入从(h,w)变成了(h',w'),同样超参数的转置卷积则从(h',w')变成(h,w)

与常规卷积不同,在转置卷积中,填充被应用于的输出(常规卷积将填充应用于输入)。 例如,当将高和宽两侧的填充数指定为1时,转置卷积的输出中将删除第一和最后的行与列。

import torch
from torch import nn
from d2l import torch as d2l


# 自己定义转置卷积函数
def trans_conv(X, K):
    h, w = K.shape
    Y = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            Y[i:i + h, j:j + w] += X[i, j] * K
    return Y


X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
print(trans_conv(X, K))
# 使用PytorchAPI
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False)
tconv.weight.data = K
print(tconv(X))
# 填充,对输出进行删除
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, padding=1, bias=False)
tconv.weight.data = K
print(tconv(X))
# 这里增大stride是把输出变大
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, stride=2, bias=False)
tconv.weight.data = K
tconv(X)
# 多通道和卷积的多通道同理
X = torch.rand(size=(1, 10, 16, 16))
conv = nn.Conv2d(10, 20, kernel_size=5, padding=2, stride=3)
tconv = nn.ConvTranspose2d(20, 10, kernel_size=5, padding=2, stride=3)
print(tconv(conv(X)).shape == X.shape)

对于多个输入和输出通道,转置卷积与常规卷积以相同方式运作。

如果步为s,填充为s/2(s/2是整数)且卷积核的高宽为2s,转置卷积将会将输出放大s倍。

转置卷积是一种卷积,

全连接卷积神经网络FCN

FCN深度神经网络做语义分割的奠基性工程

它用转置卷积层来替换CNN最后的全局连接层(还有全局池化层),从而实现每个像素的预测。这里1x1的卷积层是用来降低空间维度的。转置卷积后会输出K类,对应K个类型的预测值。

在图像处理中我们需要将图像放大,即上采样(upsampling)。双线性差值(bilinear interpolation)是常用的上采样方式之一,经常用于初始化转置卷积层。

即调整卷积核的输出让他和双线性差值的输出保持一致,的参数来作为卷积核的初始化权重。

上采样指的是将低分辨率图像转换成高分辨率的过程。

在计算loss时交叉熵函数的输出将会是一个矩阵,然后在矩阵的高上做均值,在宽上做均值

import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# 使用imageNet数据集上预训练的Resnet-18模型来提取特征
pretrained_net = torchvision.models.resnet18(pretrained=True)
# 查看最后三层
print(list(pretrained_net.children())[-3:])
net = nn.Sequential(*list(pretrained_net.children())[:-2])
X = torch.rand(size=(1, 3, 320, 480))
print(net(X).shape)
# 构造fcn
num_classes = 21
# 输入是512,
net.add_module('final_conv', nn.Conv2d(512, num_classes, kernel_size=1))
# 输入输出都是21,放大32倍,其他参数是为了放大32倍的时候宽高不变,16是宽高不变的最小值,同时还让窗口有一定重叠度。
net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes, kernel_size=64, padding=16, stride=32))


def bilinear_kernel(in_channels, out_channels, kernel_size):
    factor = (kernel_size + 1) // 2
    if kernel_size % 2 == 1:
        center = factor - 1
    else:
        center = factor - 0.5
    og = (torch.arange(kernel_size).reshape(-1, 1),
          torch.arange(kernel_size).reshape(1, -1))
    filt = (1 - torch.abs(og[0] - center) / factor) * \
           (1 - torch.abs(og[1] - center) / factor)
    weight = torch.zeros((in_channels, out_channels,
                          kernel_size, kernel_size))
    weight[range(in_channels), range(out_channels), :, :] = filt
    return weight


# 用双线性差值的上采样设计研它有转置卷积的实现,构造输入的高和宽放大2倍的转置卷积层,用bilinear_kernel函数初始化
conv_trans = nn.ConvTranspose2d(3, 3, kernel_size=4, padding=1, stride=2, bias=False)
conv_trans.weight.data.copy_(bilinear_kernel(3, 3, 4))
# 读取图像X,将上采样的结果记作Y。为了打印图像,我们需要调整通道维的位置。

img = torchvision.transforms.ToTensor()(d2l.Image.open('../img/catdog.png'))
X = img.unsqueeze(0)  # 添加一个新的维度
Y = conv_trans(X)  # 转置卷积
out_img = Y[0].permute(1, 2, 0).detach()
# 可以看到,转置卷积层将图像的高和宽分别放大了2倍。和原图看上去没什么俩样
d2l.set_figsize()
print('input image shape:', img.permute(1, 2, 0).shape)
d2l.plt.imshow(img.permute(1, 2, 0));
print('output image shape:', out_img.shape)
d2l.plt.imshow(out_img)
d2l.plt.show()
# 输入是561*728*3 输出是1122*1456*3
W = bilinear_kernel(num_classes, num_classes, 64)
# 写到FCN最后一层的权重初始化
net.transpose_conv.weight.data.copy_(W)
# 读取数据集
batch_size, crop_size = 32, (320, 480)
train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size)


# 之前的每个样本是一个值,现在每个样本是一个矩阵
def loss(inputs, targets):
    # 每个像素做均值
    return F.cross_entropy(inputs, targets, reduction='none').mean(1).mean(1)


# 开始训练,loss下降的比较慢,这是因为背景容易区分,但是物体和背景的边缘不好区分,而且转置卷积的计算量很大
num_epochs, lr, wd, devices = 5, 0.001, 1e-3, d2l.try_all_gpus()
trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)

样式迁移

将样式图片的样式迁移到图片上

风格迁移常用的损失函数由3部分组成:

  1. 内容损失使合成图像与内容图像在内容特征上接近;
  2. 风格损失使合成图像与风格图像在风格特征上接近;
  3. 全变分损失则有助于减少合成图像中的噪点。

换种方式理解转置卷积

它将输入和核进行了重排列

同卷积一般是做下采样的不同,它用来做上采样

如果卷积将输入从h,w变成了h‘,w',同样超参数下他将h',w’变成 h,w(逆变换)

首先来看填充为0步幅为1时,转置卷积等于如下操作

  • 将输入填充k-1(k是核窗口)
  • 将核矩阵上下左右翻转
  • 然后做正常卷积(填充0、步幅1)

当填充为p步幅为1时

输入填充k-p-1(k是核窗口),将矩阵上下左右翻转,做正常卷积(图为填充0,步幅1)


当填充为p,步幅为s时

  • 在行和列之间插入s-1行或列
  • 将输入填充k-p-1(k是核窗口)
  • 将核矩阵上下左右翻转
  • 然后做正常卷积。(填充0、步幅1)

形状换算,输入高宽为n,核为k,填充为p,步幅为s

转置卷积n'=sn+k-2p-s

如果让高宽成倍增加那么,k=2p+s,比如k=64,s为32,那么p就是16

数学上有反卷积deconvolution,是指卷积的逆运算如果Y=conv(X,K),那么X=deconv(Y,K)

反卷积很少用在深度学习中

Last modification:August 12, 2024
如果觉得我的文章对你有用,请随意赞赏