text to image(八):《Image Generation from Scene Graphs》
最近在翻閱文本生成圖像的相關(guān)工作,目前比較新的有突破性的工作是李飛飛工作團(tuán)隊(duì)18年cvpr發(fā)表的《Image Generation from Scene Graphs》 。
?????? 論文地址:https://arxiv.org/abs/1804.01622
?????? 源碼地址: https://github.com/google/sg2im
?????? 這篇主要就是介紹該論文的工作,穿插對(duì)部分代碼的理解和講解。看完代碼以后拜服Justin Johnson大神,真厲害!
?
一、相關(guān)工作
?????? 先前已經(jīng)有了很多文本生成圖像的方法,比較具有代表性的是StackGAN和StackGAN++(會(huì)在其它博客中給出介紹)。StackGAN存在的比較突出的問(wèn)題是不能處理比較復(fù)雜的文本。比如句子為:A sheep by another sheep standing on the grass with sky above and a boat in the ocean by a tree behind the sheep.
?????? ?結(jié)果如下圖所示:
? ? ? ??? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
左圖是StackGAN生成的結(jié)果,右圖是李飛飛小組提出的新方法的結(jié)果。右邊的效果明顯更好。
?
二、基本思想
?????不同于先前的方法,李飛飛小組提出可以使用場(chǎng)景圖作為中間媒介。即由原本的
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?文本----->圖像(也就是RNN+GAN的直接搭配)??
? ? ??轉(zhuǎn)化為
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 文本--→場(chǎng)景圖--→圖像。
? ? ??首先的問(wèn)題是:什么是場(chǎng)景圖,場(chǎng)景圖怎么得到?
? ? ??場(chǎng)景圖是一種可以用來(lái)表示文本或者圖像結(jié)構(gòu)的表述。如圖二所示,即為圖一所對(duì)應(yīng)的文本的場(chǎng)景圖。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
?????? 可以看到,場(chǎng)景圖將場(chǎng)景表示為有向圖,其中節(jié)點(diǎn)(紅色)是對(duì)象,邊(藍(lán)色)給出對(duì)象之間的關(guān)系。
? ? ? ?關(guān)于場(chǎng)景圖的獲取:一些數(shù)據(jù)集中提供了與圖像配套的場(chǎng)景圖,例如Visual Genome數(shù)據(jù)集。大部分場(chǎng)景圖的工作都是基于此數(shù)據(jù)集的。第二種方法是使用句子或者圖像直接生成場(chǎng)景圖。從句子中生成的場(chǎng)景圖的工作有:《Generating Semantically Precise Scene Graphs from Textual Descriptions for Improved Image Retrieval》。從圖像中生成場(chǎng)景圖的工作有:《Visual relationship detection with language priors》 、 《Pixels to graphs by associative embedding》 、《Scene graph generation by iterative message passing》、《On support relations and semantic scene graphs》。這里暫時(shí)不做詳細(xì)的介紹。
?????? ?獲得場(chǎng)景圖之后,后續(xù)的處理如下圖所示:
? ? ? ?首先將場(chǎng)景圖(Scene graph)輸入圖卷積網(wǎng)絡(luò)(Graph Convolution)獲得對(duì)象的嵌入向量(Object features)(嵌入向量是什么,后續(xù)有解釋)。獲得的嵌入向量輸入對(duì)象布局網(wǎng)絡(luò)。對(duì)象布局網(wǎng)絡(luò)預(yù)測(cè)對(duì)象的bounding boxes和segmentation masks。得到scene layout(場(chǎng)景布局)。將其輸入級(jí)聯(lián)細(xì)化網(wǎng)絡(luò)(Cascaded Refinement Network)得到最后的輸出圖像。
? ? ? ? 每個(gè)模塊的具體實(shí)現(xiàn)會(huì)在后面配合代碼進(jìn)行介紹。這里給出嵌入向量的概念:我們使用不同的向量來(lái)表示不同的對(duì)象(詞匯),最直接的做法是使用one-hot的形式,但是當(dāng)詞庫(kù)特別大,詞向量會(huì)十分冗長(zhǎng)。而且兩個(gè)關(guān)系很接近的詞匯對(duì)應(yīng)的向量(比如 “國(guó)王”和“王后”)之間的關(guān)系并不能通過(guò)one-hot的詞向量表示出來(lái)。
? ? ? ? 所以引入嵌入向量(詞嵌入)來(lái)表示不同對(duì)象。首先,它也是一個(gè)向量,但是長(zhǎng)度比較短,而且是一個(gè)密集向量。其次,它可以表示關(guān)系相近的對(duì)象之間的關(guān)系(比如“國(guó)王”-“男人”+“女人”約等于“王后” ) 這種關(guān)系是通過(guò)距離表現(xiàn)出來(lái)的。比如“貓”和“狗”向量之間的距離比較近,但是“貓”和“天空”距離就比較遠(yuǎn)。
三、數(shù)據(jù)集介紹
?????? 在介紹具體的模型結(jié)構(gòu)前,我們先介紹一下實(shí)驗(yàn)中用到的數(shù)據(jù)集。源碼中可選的數(shù)據(jù)集有兩個(gè):Visual Genome數(shù)據(jù)集和COCO數(shù)據(jù)集。
? ? ?Visual Genome 數(shù)據(jù)集包括 7 個(gè)主要部分:
? ? ? 區(qū)域描述 --------對(duì)圖像的不同區(qū)域進(jìn)行描述。這些區(qū)域是由邊框坐標(biāo)限定的,區(qū)域之間允許有重復(fù)。數(shù)據(jù)集中平均對(duì)每一張圖片有 42 種區(qū)域描述。每一個(gè)描述都是一個(gè)短語(yǔ)包含著從 1 到 16 單詞長(zhǎng)度,以描述這個(gè)區(qū)域。
? ? ? ?對(duì)象---------------平均每張圖片包含21個(gè)物體,每個(gè)物體周圍有一個(gè)邊框。
? ? ? 屬性---------------平均每張圖片有16個(gè)屬性。一個(gè)物體可以有0個(gè)或是更多的屬性。屬性可以是顏色(比如yellow),狀態(tài)(比如standing),等等。
? ? ? ?關(guān)系----------------“關(guān)系”將兩個(gè)物體關(guān)聯(lián)到一起。
? ? ? ?區(qū)域圖-------------將從區(qū)域描述中提取的物體、屬性、以及關(guān)系結(jié)合在一起。表述該區(qū)域
? ? ? ?場(chǎng)景圖-------------一整幅圖片中所有的物體、屬性、以及關(guān)系的表示
? ? ? 問(wèn)答對(duì)-------------每張圖片都有兩類問(wèn)答:基于整張圖片的隨意問(wèn)答(freeform QAs),以及基于選定區(qū)域的區(qū)域問(wèn)答(region-based QAs)。在本實(shí)驗(yàn)中用不到,所以不做過(guò)多解釋。
? ? ? ? 此外,每一個(gè)對(duì)象、屬性、關(guān)系在WordNet中都有自己規(guī)范化的ID。
COCO的數(shù)據(jù)標(biāo)注信息包括:
- 類別標(biāo)志
- 類別數(shù)量區(qū)分
- 像素級(jí)的分割
?
? ? ? ?2014年版本的數(shù)據(jù)為例,一共有20G左右的圖片和500M左右的標(biāo)簽文件。標(biāo)簽文件標(biāo)記了每個(gè)segmentation+bounding box的精確坐標(biāo),其精度均為小數(shù)點(diǎn)后兩位。一個(gè)目標(biāo)的標(biāo)簽示意如下:
{"segmentation":[[392.87, 275.77, 402.24, 284.2, 382.54, 342.36, 375.99, 356.43, 372.23, 357.37, 372.23, 397.7, 383.48, 419.27,407.87, 439.91, 427.57, 389.25, 447.26, 346.11, 447.26, 328.29, 468.84, 290.77,472.59, 266.38], [429.44,465.23, 453.83, 473.67, 636.73, 474.61, 636.73, 392.07, 571.07, 364.88, 546.69,363.0]], "area": 28458.996150000003, "iscrowd": 0,"image_id": 503837, "bbox": [372.23, 266.38, 264.5,208.23], "category_id": 4, "id": 151109}
?
?
四、模型結(jié)構(gòu)
? ? ? ?依舊先上這張整體結(jié)構(gòu)圖。;另外,下文提到的向量具體數(shù)值均是為了便于理解,并不完全與代碼運(yùn)行后的實(shí)際數(shù)值相同。
圖卷積網(wǎng)絡(luò)(GCN)
? ? ? ?由Thomas Kpif于2017年在論文《Semi-supervised Classification with Graph Convolutional Networks》。Thomas Kpif的這篇論文屬于譜卷積,即將卷積網(wǎng)絡(luò)的濾波器與圖信號(hào)同時(shí)搬移到傅里葉域以后進(jìn)行處理。
? ? ? ? 原論文沒(méi)看太懂,數(shù)學(xué)推導(dǎo)可以參https://blog.csdn.net/chensi1995/article/details/77232019值得注意的是,圖嵌入(graph embedding)、網(wǎng)絡(luò)嵌入(network embedding)、網(wǎng)絡(luò)表示學(xué)習(xí)(network representation learning),這三個(gè)概念從原理上來(lái)說(shuō)其實(shí)表達(dá)的是同一件事,核心思想就是“通過(guò)深度學(xué)習(xí)技術(shù)將圖中的節(jié)點(diǎn)(或邊)映射為向量空間中的點(diǎn),進(jìn)而可以對(duì)向量空間中的點(diǎn)進(jìn)行聚類、分類等處理”。圖卷積神經(jīng)網(wǎng)絡(luò)就屬于圖嵌入技術(shù)的一種。
?????? 也就是說(shuō),圖卷積網(wǎng)絡(luò)的目的是把對(duì)象(節(jié)點(diǎn))、關(guān)系(邊)映射為嵌入向量。
?????? 具體原理可以參考https://blog.csdn.net/tMb8Z9Vdm66wH68VX1/article/details/78705916。 為方便后續(xù)理解,這里給出部分內(nèi)容的截圖。
Hl即為第l個(gè)隱含層,H0就是輸入層。
?
?
可以看到GCN可以有一到多層。
?
下面分析源碼:
? ? ? 首先讀取場(chǎng)景圖,在代碼中場(chǎng)景圖的表示是一個(gè)由若干字典組成的列表。一幅圖像可能有一到多個(gè)場(chǎng)景圖表述。
? ? ? 字典有兩個(gè)key: 'objects' 和'relationships' 。這里給出一個(gè)scene graph:{'objects': ['sky', 'grass', 'zebra'], 'relationships': [[0, 'above', 1], [2, 'standing on', 1]]}。對(duì)于擁有多個(gè)字典(場(chǎng)景圖)的列表(圖像),在列表中從前到后字典逐漸變復(fù)雜,對(duì)象數(shù)目和關(guān)系數(shù)目增多。在relationship的列表中,目標(biāo)用索引表示。一幅圖像中有N個(gè)目標(biāo),則在所有的字典中,目標(biāo)的索引從0到N-1。
? ? ? ?場(chǎng)景圖的處理是encode_scene_graphs函數(shù)。函數(shù)輸入是單個(gè)場(chǎng)景圖的字典或者包含了多個(gè)字典的列表,輸出是元組(objs, triples, obj_to_img)。之前講到在VG數(shù)據(jù)集中,所有的對(duì)象、屬性、關(guān)系都有規(guī)范化的索引,我們暫且稱之為詞庫(kù)索引。objs, triples, obj_to_img都是 列表,objs表示所有所有場(chǎng)景圖中出現(xiàn)的所有目標(biāo)的詞庫(kù)索引,triples表示所有場(chǎng)景圖中每一張場(chǎng)景圖中目標(biāo)之間的關(guān)系,obj_to_img與objs長(zhǎng)度相同,用于標(biāo)注哪些目標(biāo)屬于哪些場(chǎng)景圖。比如:
? ? ?? objs:[11, 17, 130, 0, 11, 17, 129, 0]??????? 最后用0來(lái)代表'__image__'
? ? ?? triples:[[0, 4, 1], [2, 19, 1], [0, 0, 3], [1, 0, 3], [2, 0, 3], [4, 4, 5]]
? ? ? ?obj_to_img:[0, 0, 0, 0, 1, 1, 1, 1,…..N]?? 總共有N個(gè)場(chǎng)景圖
? ? ? ?相關(guān)代碼:
def encode_scene_graphs(self, scene_graphs):"""Encode one or more scene graphs using this model's vocabulary. Inputs tothis method are scene graphs represented as dictionaries like the following:{"objects": ["cat", "dog", "sky"],"relationships": [[0, "next to", 1],[0, "beneath", 2],[2, "above", 1],]}This scene graph has three relationshps: cat next to dog, cat beneath sky,and sky above dog.Inputs:- scene_graphs: A dictionary giving a single scene graph, or a list ofdictionaries giving a sequence of scene graphs.Returns a tuple of LongTensors (objs, triples, obj_to_img) that have thesame semantics as self.forward. The returned LongTensors will be on thesame device as the model parameters."""if isinstance(scene_graphs, dict):# We just got a single scene graph, so promote it to a listscene_graphs = [scene_graphs]objs, triples, obj_to_img = [], [], []obj_offset = 0for i, sg in enumerate(scene_graphs):#對(duì)于單幅圖像 有好幾個(gè)場(chǎng)景圖 每次進(jìn)一個(gè)場(chǎng)景圖# Insert dummy __image__ object and __in_image__ relationshipssg['objects'].append('__image__')image_idx = len(sg['objects']) - 1for j in range(image_idx):sg['relationships'].append([j, '__in_image__', image_idx])#首先是對(duì)場(chǎng)景圖進(jìn)行處理,除了原本的目標(biāo)以外,加入新的目標(biāo) ,也就是總體的'__image__'#加入了新的目標(biāo),就需要更新關(guān)系列表 也就是 之間的每一個(gè)目標(biāo) 都'__in_image__'在圖像里#舉例:sg['objects']:['sky', 'grass', 'sheep', '__image__']#sg['relationships']: [[0, 'above', 1], [2, 'standing on', 1], [0, #'__in_image__', 3], [1, '__in_image__', 3], [2, '__in_image__', 3]]#0 1 .....len(sg['objects']) - 2 分別是原圖中目標(biāo)的索引 #image_idx = len(sg['objects']) - 1 是image的索引for obj in sg['objects']: obj_idx = self.vocab['object_name_to_idx'].get(obj, None) #獲取場(chǎng)景圖中目標(biāo)對(duì)應(yīng)的詞庫(kù)索引if obj_idx is None:raise ValueError('Object "%s" not in vocab' % obj)objs.append(obj_idx) #對(duì)應(yīng)的詞庫(kù)索引 最后用0來(lái)代表'__image__' [11, 17, 130, 0, 11, 17, 129, 0]obj_to_img.append(i) #用于標(biāo)注 對(duì)于當(dāng)前的圖像 這是第幾個(gè)場(chǎng)景圖 [0, 0, 0, 0, 1, 1, 1, 1]for s, p, o in sg['relationships']:pred_idx = self.vocab['pred_name_to_idx'].get(p, None) #獲取關(guān)系對(duì)應(yīng)的詞庫(kù)索引if pred_idx is None:raise ValueError('Relationship "%s" not in vocab' % p)triples.append([s + obj_offset, pred_idx, o + obj_offset])#描述目標(biāo)之間的關(guān)系 不同場(chǎng)景圖的不同目標(biāo) 用不同的索引標(biāo)注#[[0, 4, 1], [2, 19, 1], [0, 0, 3], [1, 0, 3], [2, 0, 3], [4, 4, 5], [6, 19, 5], [4, 0, 7], [5, 0, 7], [6, 0, 7]]obj_offset += len(sg['objects'])device = next(self.parameters()).deviceobjs = torch.tensor(objs, dtype=torch.int64, device=device)#所有場(chǎng)景圖中出現(xiàn)的所有目標(biāo)的詞庫(kù)索引triples = torch.tensor(triples, dtype=torch.int64, device=device) #所有場(chǎng)景圖中每一張場(chǎng)景圖中目標(biāo)之間的關(guān)系obj_to_img = torch.tensor(obj_to_img, dtype=torch.int64, device=device)# 標(biāo)注哪些目標(biāo)屬于哪些場(chǎng)景圖return objs, triples, obj_to_img將對(duì)象和關(guān)系使用128維的詞嵌入表示,將其輸入圖卷積網(wǎng)絡(luò),得到嵌入向量。
相關(guān)代碼:
obj_vecs = self.obj_embeddings(objs) #[42,128] 將每個(gè)目標(biāo)用128維的嵌入向量表示 obj_vecs_orig = obj_vecs pred_vecs = self.pred_embeddings(p) #[63,128] 將每種關(guān)系也用128維的嵌入向量表示if isinstance(self.gconv, nn.Linear): #如果沒(méi)有設(shè)計(jì)場(chǎng)景圖卷積網(wǎng)絡(luò) 而是直接加了全連接層 obj_vecs = self.gconv(obj_vecs) #得到128維度的輸出向量 else:obj_vecs, pred_vecs = self.gconv(obj_vecs, pred_vecs, edges) #如果只有一層if self.gconv_net is not None:obj_vecs, pred_vecs = self.gconv_net(obj_vecs, pred_vecs, edges) #如果有多層 就把剛才那一層的輸出作為輸入#[42,128] [63,128] 這里42是對(duì)象的數(shù)量 63是關(guān)系的數(shù)量2、 對(duì)象布局網(wǎng)絡(luò)
對(duì)象布局網(wǎng)絡(luò)由兩部分組成,一部分是Mask regression network,一部分是Box regression network,如下圖所示:
?
?
(1)預(yù)測(cè)對(duì)象的bounding_box:
使用一個(gè)多層感知器來(lái)實(shí)現(xiàn)。包含有三層,實(shí)驗(yàn)中輸入層是128的對(duì)象嵌入向量,隱藏層的維度取了512,輸出層是4維向量,對(duì)每個(gè)對(duì)象的嵌入向量,得到bouding_box的坐標(biāo)。
代碼:
boxes_pred = self.box_net(obj_vecs)?? #[42,4]? 對(duì)于每個(gè)對(duì)象 預(yù)測(cè)它們的bounding_box
??????
(2)預(yù)測(cè)mask:
?????? 建立一個(gè)mask_net來(lái)預(yù)測(cè)mask。網(wǎng)絡(luò)的輸入是所有場(chǎng)景圖中所有對(duì)象的嵌入向量。例如一共由42個(gè)對(duì)象,輸入為[42,128,1,1],輸出為16*16的mask_pre[42,1,16,16],也就是每個(gè)對(duì)象的binary masks。網(wǎng)絡(luò)中主要進(jìn)行了上采樣、卷積等操作。如下列代碼所示:
代碼:
masks_pred = None if self.mask_net is not None:mask_scores = self.mask_net(obj_vecs.view(O, -1, 1, 1))#輸入[42,128,1,1]輸出[42,1,16,16] #對(duì)于每個(gè)對(duì)象產(chǎn)生16*16的圖像 O是對(duì)象的總數(shù)目 masks_pred = mask_scores.squeeze(1).sigmoid() #[42,16,16]def _build_mask_net(self, num_objs, dim, mask_size): #將128維的圖像變?yōu)?維的maskoutput_dim = 1layers, cur_size = [], 1while cur_size < mask_size:layers.append(nn.Upsample(scale_factor=2, mode='nearest'))#上采樣圖像成原來(lái)的四倍(長(zhǎng)寬2倍)layers.append(nn.BatchNorm2d(dim))layers.append(nn.Conv2d(dim, dim, kernel_size=3, padding=1))layers.append(nn.ReLU())cur_size *= 2if cur_size != mask_size:raise ValueError('Mask size must be a power of 2')layers.append(nn.Conv2d(dim, output_dim, kernel_size=1)) #最后的輸出圖像是單通道的layout return nn.Sequential(*layers)(3)bounding_box和mask結(jié)合生成圖像布局
作者設(shè)計(jì)了masks_to_layout網(wǎng)絡(luò)來(lái)實(shí)現(xiàn)這一功能。輸入是obj_vecs, layout_boxes, layout_masks, obj_to_img以及 想要產(chǎn)生的圖像布局的尺寸H, W。輸出是[N,D,H,W]的形式。
N是要生成的圖像batch大小,H,W是尺寸,D是通道數(shù)。具體結(jié)構(gòu)原理沒(méi)看太懂。
?????? 如下列代碼所示:
layout = masks_to_layout(obj_vecs, layout_boxes, layout_masks,obj_to_img, H, W)#輸入: obj_vecs[42,128] layout_boxes[42,4] layout_masks[42,16,16]#obj_to_img[42] H:128 W:128 #layout:[7,128,128,128]?
3、級(jí)聯(lián)網(wǎng)絡(luò)
級(jí)聯(lián)網(wǎng)絡(luò)來(lái)自論文《Photographic image synthesis with cascaded refinement networks》。在得到layout([7,128,128,128])后,作者引入了layout_noise([7, 32, 128, 128]),與其結(jié)合成([7, 160, 128, 128])輸入級(jí)聯(lián)網(wǎng)絡(luò),得到最終的三通道彩圖。
級(jí)聯(lián)網(wǎng)絡(luò)并沒(méi)有依靠生成對(duì)抗網(wǎng)絡(luò)(GAN)以訓(xùn)練generator與discriminator network的方式來(lái)做image-to-image,而是采用了一種級(jí)聯(lián)精練網(wǎng)絡(luò)Cascaded Refinement Network (CRN)來(lái)實(shí)現(xiàn)逼真街景圖的生成。每個(gè)模塊接收兩個(gè)場(chǎng)景布局作為輸入,即下采樣到模塊的輸入分辨率和來(lái)自前一個(gè)模塊的輸出。這些輸入通道連接并傳遞給一對(duì)3*3卷積層; 然后在傳遞到下一個(gè)模塊之前使用最近鄰插值對(duì)輸出進(jìn)行上采樣。 第一個(gè)模塊采用高斯噪聲z~ pz作為輸入,并且來(lái)自最后一個(gè)模塊的輸出被傳遞到兩個(gè)最終的卷積層以產(chǎn)生輸出圖像。
具體的代碼模塊比較多,這里就不再給出介紹。
代碼:
img = self.refinement_net(layout) #輸入[7, 160, 128, 128] 輸出[7, 3, 128, 128]至此,模型結(jié)構(gòu)部分介紹完畢。
五、訓(xùn)練
模型使用一對(duì)鑒別器網(wǎng)絡(luò)和訓(xùn)練圖像生成網(wǎng)絡(luò)f來(lái)生成逼真的輸出圖像。 確保生成的圖像的整體外觀是真實(shí)的; 確保圖像中的每個(gè)對(duì)象看起來(lái)都是真實(shí)的;它的輸入是一個(gè)對(duì)象的像素,使用雙線性插值裁剪并重新縮放到固定大小。除了將每個(gè)對(duì)象分類為真實(shí)或虛假之外,還確保使用輔助分類器來(lái)識(shí)別每個(gè)對(duì)象,該分類器預(yù)測(cè)對(duì)象的類別。
我們聯(lián)合訓(xùn)練圖像生成網(wǎng)絡(luò)f、鑒別器和。我們?cè)噲D最小化6個(gè)損失的加權(quán)和。
?
Box loss預(yù)測(cè)對(duì)象的位置信息,懲罰真實(shí)對(duì)象位置與預(yù)測(cè)位置的誤差。
Mask loss pixelwise cross-entropy使用像素交叉熵懲罰地面實(shí)況和預(yù)測(cè)掩模之間的差異; 不用于在Visual Genome上訓(xùn)練的模型。
Pixel loss懲罰GT圖像與生成圖像之間的L1差異。
Image adversarial loss鼓勵(lì)生成的圖像整體看起來(lái)更真實(shí)。
Object adversarial loss 鼓勵(lì)生成的對(duì)象看起來(lái)更真實(shí)。
Auxiliarly classifier loss確保每一個(gè)對(duì)象都能夠被Dobj識(shí)別。
?
訓(xùn)練部分的代碼涉及鑒別器和生成器,6種損失,比較復(fù)雜(再次膜拜大神),這里就不給出詳細(xì)的代碼理解,只寫一下訓(xùn)練主函數(shù)的基本流程。
?
總結(jié)
以上是生活随笔為你收集整理的text to image(八):《Image Generation from Scene Graphs》的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 图片 360度旋转动画
- 下一篇: 分享一个边看视频就能边练口语的学习网站,