使用PyTorch進(jìn)行圖像風(fēng)格轉(zhuǎn)換

譯者:bdqfork

作者: Alexis Jacq

簡介

本教程主要講解如何實(shí)現(xiàn)由Leon A. Gatys歹茶,Alexander S. Ecker和Matthias Bethge提出的 Neural-Style 算法褒傅。Neural-Style或者叫Neural-Transfer,可以讓你使用一種新的風(fēng)格將指定的圖片進(jìn)行重構(gòu)。這個(gè)算法使用三張圖片走搁,一張輸入圖片建邓,一張內(nèi)容圖片和一張風(fēng)格圖片,并將輸入的圖片變得與內(nèi)容圖片相似春缕,且擁有風(fēng)格圖片的優(yōu)美風(fēng)格盗胀。

content1

基本原理

原理很簡單:我們定義兩個(gè)間距,一個(gè)用于內(nèi)容D_C淡溯,另一個(gè)用于風(fēng)格D_S读整。D_C測量兩張圖片內(nèi)容的不同,而D_S用來測量兩張圖片風(fēng)格的不同咱娶。然后米间,我們輸入第三張圖片,并改變這張圖片膘侮,使其與內(nèi)容圖片的內(nèi)容間距和風(fēng)格圖片的風(fēng)格間距最小化∏現(xiàn)在,我們可以導(dǎo)入必要的包琼了,開始圖像風(fēng)格轉(zhuǎn)換逻锐。

導(dǎo)包并選擇設(shè)備

下面是一張實(shí)現(xiàn)圖像風(fēng)格轉(zhuǎn)換所需包的清單。

  • torch, torch.nn, numpy (使用PyTorch進(jìn)行風(fēng)格轉(zhuǎn)換必不可少的包)
  • torch.optim (高效的梯度下降)
  • PIL, PIL.Image, matplotlib.pyplot (加載和展示圖片)
  • torchvision.transforms (將PIL圖片轉(zhuǎn)換成張量)
  • torchvision.models (訓(xùn)練或加載預(yù)訓(xùn)練模型)
  • copy (對模型進(jìn)行深度拷貝雕薪;系統(tǒng)包)
from __future__ import print_function

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

from PIL import Image
import matplotlib.pyplot as plt

import torchvision.transforms as transforms
import torchvision.models as models

import copy

下一步昧诱,我們選擇用哪一個(gè)設(shè)備來運(yùn)行神經(jīng)網(wǎng)絡(luò),導(dǎo)入內(nèi)容和風(fēng)格圖片所袁。在大量圖片上運(yùn)行圖像風(fēng)格算法需要很長時(shí)間盏档,在GPU上運(yùn)行可以加速。我們可以使用torch.cuda.is_available()來判斷是否有可用的GPU燥爷。下一步蜈亩,我們在整個(gè)教程中使用 torch.device 懦窘。 .to(device) 方法也被用來將張量或者模型移動(dòng)到指定設(shè)備。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

加載圖片

現(xiàn)在我們將導(dǎo)入風(fēng)格和內(nèi)容圖片稚配。原始的PIL圖片的值介于0到255之間畅涂,但是當(dāng)轉(zhuǎn)換成torch張量時(shí),它們的值被轉(zhuǎn)換成0到1之間道川。圖片也需要被重設(shè)成相同的維度午衰。一個(gè)重要的細(xì)節(jié)是,注意torch庫中的神經(jīng)網(wǎng)絡(luò)用來訓(xùn)練的張量的值為0到1之間愤惰。如果你嘗試將0到255的張量圖片加載到神經(jīng)網(wǎng)絡(luò)苇经,然后激活的特征映射將不能偵測到目標(biāo)內(nèi)容和風(fēng)格。然而宦言,Caffe庫中的預(yù)訓(xùn)練網(wǎng)絡(luò)用來訓(xùn)練的張量值為0到255之間的圖片扇单。

注意

這是一個(gè)下載本教程需要用到的圖片的鏈接: picasso.jpgdancing.jpg。下載這兩張圖片并且將它們添加到你當(dāng)前工作目錄中的 images 文件夾奠旺。

# desired size of the output image
imsize = 512 if torch.cuda.is_available() else 128  # use small size if no gpu

loader = transforms.Compose([
    transforms.Resize(imsize),  # scale imported image
    transforms.ToTensor()])  # transform it into a torch tensor

def image_loader(image_name):
    image = Image.open(image_name)
    # fake batch dimension required to fit network's input dimensions
    image = loader(image).unsqueeze(0)
    return image.to(device, torch.float)

style_img = image_loader("./data/images/neural-style/picasso.jpg")
content_img = image_loader("./data/images/neural-style/dancing.jpg")

assert style_img.size() == content_img.size(), \
    "we need to import style and content images of the same size"

現(xiàn)在蜘澜,讓我們創(chuàng)建一個(gè)方法,通過重新將圖片轉(zhuǎn)換成PIL格式來展示响疚,并使用plt.imshow展示它的拷貝鄙信。我們將嘗試展示內(nèi)容和風(fēng)格圖片來確保它們被正確的導(dǎo)入。

unloader = transforms.ToPILImage()  # reconvert into PIL image

plt.ion()

def imshow(tensor, title=None):
    image = tensor.cpu().clone()  # we clone the tensor to not do changes on it
    image = image.squeeze(0)      # remove the fake batch dimension
    image = unloader(image)
    plt.imshow(image)
    if title is not None:
        plt.title(title)
    plt.pause(0.001) # pause a bit so that plots are updated

plt.figure()
imshow(style_img, title='Style Image')

plt.figure()
imshow(content_img, title='Content Image')

  • https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_001.png
  • https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_002.png
    https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_002.png

損失函數(shù)

內(nèi)容損失

內(nèi)容損失是一個(gè)表示一層內(nèi)容間距的加權(quán)版本忿晕。這個(gè)方法使用網(wǎng)絡(luò)中的L層的特征映射F_XL装诡,該網(wǎng)絡(luò)處理輸入X并返回在圖片X和內(nèi)容圖片C之間的加權(quán)內(nèi)容間距W_CL*D_C^L(X,C)。該方法必須知道內(nèi)容圖片(F_CL)的特征映射來計(jì)算內(nèi)容間距践盼。我們使用一個(gè)以F_CL作為構(gòu)造參數(shù)輸入的torch模型來實(shí)現(xiàn)這個(gè)方法鸦采。間距||F_XL-F_CL||^2是兩個(gè)特征映射集合之間的平均方差,可以使用nn.MSELoss來計(jì)算咕幻。

我們將直接添加這個(gè)內(nèi)容損失模型到被用來計(jì)算內(nèi)容間距的卷積層之后渔伯。這樣每一次輸入圖片到網(wǎng)絡(luò)中時(shí),內(nèi)容損失都會在目標(biāo)層被計(jì)算肄程。而且因?yàn)樽詣?dòng)求導(dǎo)的緣故锣吼,所有的梯度都會被計(jì)算。現(xiàn)在蓝厌,為了使內(nèi)容損失層透明化玄叠,我們必須定義一個(gè)forward方法來計(jì)算內(nèi)容損失,同時(shí)返回該層的輸入拓提。計(jì)算的損失作為模型的參數(shù)被保存诸典。

class ContentLoss(nn.Module):

    def __init__(self, target,):
        super(ContentLoss, self).__init__()
        # we 'detach' the target content from the tree used
        # to dynamically compute the gradient: this is a stated value,
        # not a variable. Otherwise the forward method of the criterion
        # will throw an error.
        self.target = target.detach()

    def forward(self, input):
        self.loss = F.mse_loss(input, self.target)
        return input

注意

重要細(xì)節(jié):盡管這個(gè)模型的名稱被命名為 ContentLoss, 它不是一個(gè)真實(shí)的PyTorch損失方法。如果你想要定義你的內(nèi)容損失為PyTorch Loss方法崎苗,你必須創(chuàng)建一個(gè)PyTorch自動(dòng)求導(dǎo)方法來手動(dòng)的在backward方法中重計(jì)算/實(shí)現(xiàn)梯度.

風(fēng)格損失

風(fēng)格損失模型與內(nèi)容損失模型的實(shí)現(xiàn)方法類似狐粱。它要作為一個(gè)網(wǎng)絡(luò)中的透明層,來計(jì)算相應(yīng)層的風(fēng)格損失胆数。為了計(jì)算風(fēng)格損失肌蜻,我們需要計(jì)算Gram矩陣G_XL。Gram矩陣是將給定矩陣和它的轉(zhuǎn)置矩陣的乘積必尼。在這個(gè)應(yīng)用中蒋搜,給定的矩陣是L層特征映射F_XL的重塑版本。F_XL被重塑成F?_XL判莉,一個(gè)KxN的矩陣豆挽,其中K是L層特征映射的數(shù)量,N是任何向量化特征映射F_XL^K的長度券盅。例如帮哈,第一行的F?_XL與第一個(gè)向量化的F_XL^1

最后锰镀,Gram矩陣必須通過將每一個(gè)元素除以矩陣中所有元素的數(shù)量進(jìn)行標(biāo)準(zhǔn)化娘侍。標(biāo)準(zhǔn)化是為了消除擁有很大的N維度F?_XL在Gram矩陣中產(chǎn)生的很大的值。這些很大的值將在梯度下降的時(shí)候泳炉,對第一層(在池化層之前)產(chǎn)生很大的影響憾筏。風(fēng)格特征往往在網(wǎng)絡(luò)中更深的層,所以標(biāo)準(zhǔn)化步驟是很重要的花鹅。

def gram_matrix(input):
    a, b, c, d = input.size()  # a=batch size(=1)
    # b=number of feature maps
    # (c,d)=dimensions of a f. map (N=c*d)

    features = input.view(a * b, c * d)  # resise F_XL into \hat F_XL

    G = torch.mm(features, features.t())  # compute the gram product

    # we 'normalize' the values of the gram matrix
    # by dividing by the number of element in each feature maps.
    return G.div(a * b * c * d)

現(xiàn)在風(fēng)格損失模型看起來和內(nèi)容損失模型很像氧腰。風(fēng)格間距也用G_XLG_SL之間的均方差來計(jì)算。

class StyleLoss(nn.Module):

    def __init__(self, target_feature):
        super(StyleLoss, self).__init__()
        self.target = gram_matrix(target_feature).detach()

    def forward(self, input):
        G = gram_matrix(input)
        self.loss = F.mse_loss(G, self.target)
        return input

導(dǎo)入模型

現(xiàn)在我們需要導(dǎo)入預(yù)訓(xùn)練的神經(jīng)網(wǎng)絡(luò)刨肃。我們將使用19層的VGG網(wǎng)絡(luò)古拴,就像論文中使用的一樣。

PyTorch的VGG模型實(shí)現(xiàn)被分為了兩個(gè)字Sequential模型:features(包含卷積層和池化層)和classifier(包含全連接層)之景。我們將使用features模型斤富,因?yàn)槲覀冃枰恳粚泳矸e層的輸出來計(jì)算內(nèi)容和風(fēng)格損失。在訓(xùn)練的時(shí)候有些層會有和評估不一樣的行為锻狗,所以我們必須用.eval()將網(wǎng)絡(luò)設(shè)置成評估模式满力。

cnn = models.vgg19(pretrained=True).features.to(device).eval()

此外,VGG網(wǎng)絡(luò)通過使用mean=[0.485, 0.456, 0.406]和std=[0.229, 0.224, 0.225]參數(shù)來標(biāo)準(zhǔn)化圖片的每一個(gè)通道轻纪,并在圖片上進(jìn)行訓(xùn)練油额。因此,我們將在把圖片輸入神經(jīng)網(wǎng)絡(luò)之前刻帚,先使用這些參數(shù)對圖片進(jìn)行標(biāo)準(zhǔn)化潦嘶。

cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device)

# create a module to normalize input image so we can easily put it in a
# nn.Sequential
class Normalization(nn.Module):
    def __init__(self, mean, std):
        super(Normalization, self).__init__()
        # .view the mean and std to make them [C x 1 x 1] so that they can
        # directly work with image Tensor of shape [B x C x H x W].
        # B is batch size. C is number of channels. H is height and W is width.
        self.mean = torch.tensor(mean).view(-1, 1, 1)
        self.std = torch.tensor(std).view(-1, 1, 1)

    def forward(self, img):
        # normalize img
        return (img - self.mean) / self.std

一個(gè)Sequential模型包含一個(gè)順序排列的子模型序列。例如崇众,vff19.features包含一個(gè)以正確的深度順序排列的序列(Conv2d, ReLU, MaxPool2d, Conv2d, ReLU…)掂僵。我們需要將我們自己的內(nèi)容損失和風(fēng)格損失層在感知到卷積層之后立即添加進(jìn)去航厚。因此,我們必須創(chuàng)建一個(gè)新的Sequential模型锰蓬,并正確的插入內(nèi)容損失和風(fēng)格損失模型幔睬。

# desired depth layers to compute style/content losses :
content_layers_default = ['conv_4']
style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']

def get_style_model_and_losses(cnn, normalization_mean, normalization_std,
                               style_img, content_img,
                               content_layers=content_layers_default,
                               style_layers=style_layers_default):
    cnn = copy.deepcopy(cnn)

    # normalization module
    normalization = Normalization(normalization_mean, normalization_std).to(device)

    # just in order to have an iterable access to or list of content/syle
    # losses
    content_losses = []
    style_losses = []

    # assuming that cnn is a nn.Sequential, so we make a new nn.Sequential
    # to put in modules that are supposed to be activated sequentially
    model = nn.Sequential(normalization)

    i = 0  # increment every time we see a conv
    for layer in cnn.children():
        if isinstance(layer, nn.Conv2d):
            i += 1
            name = 'conv_{}'.format(i)
        elif isinstance(layer, nn.ReLU):
            name = 'relu_{}'.format(i)
            # The in-place version doesn't play very nicely with the ContentLoss
            # and StyleLoss we insert below. So we replace with out-of-place
            # ones here.
            layer = nn.ReLU(inplace=False)
        elif isinstance(layer, nn.MaxPool2d):
            name = 'pool_{}'.format(i)
        elif isinstance(layer, nn.BatchNorm2d):
            name = 'bn_{}'.format(i)
        else:
            raise RuntimeError('Unrecognized layer: {}'.format(layer.__class__.__name__))

        model.add_module(name, layer)

        if name in content_layers:
            # add content loss:
            target = model(content_img).detach()
            content_loss = ContentLoss(target)
            model.add_module("content_loss_{}".format(i), content_loss)
            content_losses.append(content_loss)

        if name in style_layers:
            # add style loss:
            target_feature = model(style_img).detach()
            style_loss = StyleLoss(target_feature)
            model.add_module("style_loss_{}".format(i), style_loss)
            style_losses.append(style_loss)

    # now we trim off the layers after the last content and style losses
    for i in range(len(model) - 1, -1, -1):
        if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
            break

    model = model[:(i + 1)]

    return model, style_losses, content_losses

下一步,我們選擇輸入圖片芹扭。你可以使用內(nèi)容圖片的副本或者白噪聲麻顶。

input_img = content_img.clone()
# if you want to use white noise instead uncomment the below line:
# input_img = torch.randn(content_img.data.size(), device=device)

# add the original input image to the figure:
plt.figure()
imshow(input_img, title='Input Image')

https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_003.png
https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_003.png

梯度下降

和算法的作者Leon Gatys的在 這里建議的一樣,我們將使用L-BFGS算法來進(jìn)行我們的梯度下降舱卡。與訓(xùn)練一般網(wǎng)絡(luò)不同辅肾,我們訓(xùn)練輸入圖片是為了最小化內(nèi)容/風(fēng)格損失。我們要?jiǎng)?chuàng)建一個(gè)PyTorch的L-BFGS優(yōu)化器optim.LBFGS轮锥,并傳入我們的圖片到其中矫钓,作為張量去優(yōu)化。

def get_input_optimizer(input_img):
    # this line to show that input is a parameter that requires a gradient
    optimizer = optim.LBFGS([input_img.requires_grad_()])
    return optimizer

最后交胚,我們必須定義一個(gè)方法來展示圖像風(fēng)格轉(zhuǎn)換份汗。對于每一次的網(wǎng)絡(luò)迭代,都將更新過的輸入傳入其中并計(jì)算損失蝴簇。我們要運(yùn)行每一個(gè)損失模型的backward方法來計(jì)算它們的梯度杯活。優(yōu)化器需要一個(gè)“關(guān)閉”方法,它重新估計(jì)模型并且返回?fù)p失熬词。

我們還有最后一個(gè)問題要解決旁钧。神經(jīng)網(wǎng)絡(luò)可能會嘗試使張量圖片的值超過0到1之間來優(yōu)化輸入。我們可以通過在每次網(wǎng)絡(luò)運(yùn)行的時(shí)候?qū)⑤斎氲闹党C正到0到1之間來解決這個(gè)問題互拾。

def run_style_transfer(cnn, normalization_mean, normalization_std,
                       content_img, style_img, input_img, num_steps=300,
                       style_weight=1000000, content_weight=1):
    """Run the style transfer."""
    print('Building the style transfer model..')
    model, style_losses, content_losses = get_style_model_and_losses(cnn,
        normalization_mean, normalization_std, style_img, content_img)
    optimizer = get_input_optimizer(input_img)

    print('Optimizing..')
    run = [0]
    while run[0] <= num_steps:

        def closure():
            # correct the values of updated input image
            input_img.data.clamp_(0, 1)

            optimizer.zero_grad()
            model(input_img)
            style_score = 0
            content_score = 0

            for sl in style_losses:
                style_score += sl.loss
            for cl in content_losses:
                content_score += cl.loss

            style_score *= style_weight
            content_score *= content_weight

            loss = style_score + content_score
            loss.backward()

            run[0] += 1
            if run[0] % 50 == 0:
                print("run {}:".format(run))
                print('Style Loss : {:4f} Content Loss: {:4f}'.format(
                    style_score.item(), content_score.item()))
                print()

            return style_score + content_score

        optimizer.step(closure)

    # a last correction...
    input_img.data.clamp_(0, 1)

    return input_img

最后歪今,我們可以運(yùn)行這個(gè)算法。

output = run_style_transfer(cnn, cnn_normalization_mean, cnn_normalization_std,
                            content_img, style_img, input_img)

plt.figure()
imshow(output, title='Output Image')

# sphinx_gallery_thumbnail_number = 4
plt.ioff()
plt.show()

https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_004.png
https://pytorch.org/tutorials/_images/sphx_glr_neural_style_tutorial_004.png

輸出:

Building the style transfer model..
Optimizing..
run [50]:
Style Loss : 4.169304 Content Loss: 4.235329

run [100]:
Style Loss : 1.145476 Content Loss: 3.039176

run [150]:
Style Loss : 0.716769 Content Loss: 2.663749

run [200]:
Style Loss : 0.476047 Content Loss: 2.500893

run [250]:
Style Loss : 0.347092 Content Loss: 2.410895

run [300]:
Style Loss : 0.263698 Content Loss: 2.358449

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颜矿,一起剝皮案震驚了整個(gè)濱河市寄猩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌骑疆,老刑警劉巖田篇,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異箍铭,居然都是意外死亡泊柬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進(jìn)店門诈火,熙熙樓的掌柜王于貴愁眉苦臉地迎上來兽赁,“玉大人,你說我怎么就攤上這事〉堆拢” “怎么了惊科?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蒲跨。 經(jīng)常有香客問我译断,道長,這世上最難降的妖魔是什么或悲? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮堪唐,結(jié)果婚禮上巡语,老公的妹妹穿的比我還像新娘。我一直安慰自己淮菠,他們只是感情好男公,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著合陵,像睡著了一般枢赔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拥知,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天踏拜,我揣著相機(jī)與錄音,去河邊找鬼低剔。 笑死速梗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的襟齿。 我是一名探鬼主播揽咕,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼缘回,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起屠尊,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锡足,沒想到半個(gè)月后漓帚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡副瀑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年弓熏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片糠睡。...
    茶點(diǎn)故事閱讀 40,872評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挽鞠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情信认,我是刑警寧澤材义,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站嫁赏,受9級特大地震影響其掂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜潦蝇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一款熬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧攘乒,春花似錦贤牛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至沽讹,卻和暖如春般卑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爽雄。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工蝠检, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盲链。 一個(gè)月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓蝇率,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刽沾。 傳聞我的和親對象是個(gè)殘疾皇子本慕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評論 2 361

推薦閱讀更多精彩內(nèi)容