傳統(tǒng)人臉互換
在深度學(xué)習(xí)出來(lái)之前芦圾,人臉互換主要是通過(guò)對(duì)比兩張臉的相似信息來(lái)進(jìn)行互換。我們可以通過(guò)特征點(diǎn)(下圖的紅色點(diǎn))來(lái)提取一張臉的眉毛、眼睛等特征信息隘道,然后匹配到另外一張人臉上们妥。如下圖所示猜扮,這種實(shí)現(xiàn)方法不需要訓(xùn)練時(shí)間,每次只需要遍歷所有的像素點(diǎn)即可监婶。但是旅赢,這樣實(shí)現(xiàn)的效果比較差,無(wú)法修改人臉的表情惑惶。
而深度學(xué)習(xí)卻可以在不修改人臉表情的情況下煮盼,做到人臉特征替換的效果。由于視頻中的人臉互換所需要的資源過(guò)多带污,并且視頻就是由一張張圖片組成的僵控,因此本次實(shí)驗(yàn)只考慮圖片中的人臉替換。我們會(huì)借用自編碼器的核心思想鱼冀,然后對(duì) DeepFake 的源碼進(jìn)行解析报破,最后實(shí)現(xiàn)川普和尼古拉斯 · 凱奇的人臉互換悠就。
數(shù)據(jù)的可視化
首先,下載實(shí)驗(yàn)所需要的數(shù)據(jù)集充易,并且完成解壓梗脾。
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1460/data.zip" # 下載數(shù)據(jù)集
!unzip -o "data.zip" # 解壓
數(shù)據(jù)集主要由兩個(gè)文件夾構(gòu)成,一個(gè)文件名為 trump 盹靴,一個(gè)為 cage 炸茧。接下來(lái),我們利用 Python 遍歷這兩個(gè)文件夾稿静,并獲得所有文件的路徑梭冠。
import os
# 遍歷directory下的所有文件,并且把他們的路徑用一個(gè)列表進(jìn)行返回
def get_image_paths(directory):
return [x.path for x in os.scandir(directory) if x.name.endswith(".jpg") or x.name.endswith(".png")]
images_A = get_image_paths("trump")
images_B = get_image_paths("cage")
print("川普?qǐng)D片個(gè)數(shù)為 {}\n凱奇的圖片個(gè)數(shù)為 {}".format(len(images_A), len(images_B)))
接下來(lái)改备,我們利用 Python 中的 OpenCV 庫(kù)妈嘹,對(duì)圖片進(jìn)行批量加載。
import cv2
import numpy as np
# 批量加載圖片绍妨,傳入的是路徑集合润脸,遍歷所有的路徑,并加載圖片
def load_images(image_paths):
iter_all_images = (cv2.imread(fn) for fn in image_paths)
# iter_all_images 是一個(gè) generator 類型他去,將它轉(zhuǎn)換成熟知的 numpy 的列表類型并返回
for i, image in enumerate(iter_all_images):
if i == 0:
# 對(duì)all_images 進(jìn)行初始,并且指定格式
all_images = np.empty(
(len(image_paths),) + image.shape, dtype=image.dtype)
all_images[i] = image
return all_images
# 每個(gè)文件夾加載三張圖片
A_images = load_images(images_A[0:3])
B_images = load_images(images_B[0:3])
print(A_images.shape)
print(B_images.shape)
這里我們分別加載了兩個(gè)人物的前三張圖片毙驯。從上面的運(yùn)行結(jié)果可以看出,每張圖片大小為256×256 灾测。那么怎樣才能一次性爆价,將這些圖片同時(shí)展示出來(lái)呢?核心思想便是將這 6 張圖片拼在一起媳搪,形成一個(gè)512×768 的圖片(一行三張铭段,一共兩行)。整體的思路如下圖所示:
首先讓我們來(lái)實(shí)現(xiàn) stack_images 函數(shù)秦爆。為了方便以后使用序愚,我們將 stack_images 寫(xiě)成一個(gè)可以將圖片集合轉(zhuǎn)變成一張圖片的函數(shù)。
# 根據(jù)所給的維度長(zhǎng)度等限,告訴調(diào)用者哪些維度應(yīng)該被放入第 0 維度爸吮,哪些應(yīng)該被轉(zhuǎn)換為第 1 維度
# 例如 (2,3,256,256,3) 則是第 0 維,第 2 維合在一起望门,轉(zhuǎn)換成新的圖片的第0維(也就是行的個(gè)數(shù))
# 第 1 維形娇,第 3 維合在一起,轉(zhuǎn)換成新的圖片的第1維(也就是列的個(gè)數(shù))
def get_transpose_axes(n):
# 根據(jù)總長(zhǎng)度的奇偶性筹误,來(lái)制定不同的情況
if n % 2 == 0:
y_axes = list(range(1, n-1, 2))
x_axes = list(range(0, n-1, 2))
else:
y_axes = list(range(0, n-1, 2))
x_axes = list(range(1, n-1, 2))
return y_axes, x_axes, [n-1]
# 可以將存儲(chǔ)多張圖片的多維集合桐早,拼成一張圖片
def stack_images(images):
images_shape = np.array(images.shape)
# new_axes 得到的是三個(gè)列表。[0,2],[1,3],[4] 告訴調(diào)用者新集合中的每個(gè)維度由舊集合中的哪些維度構(gòu)成
new_axes = get_transpose_axes(len(images_shape))
new_shape = [np.prod(images_shape[x]) for x in new_axes]
return np.transpose(
images,
axes=np.concatenate(new_axes)
).reshape(new_shape)
終于,我們可以將 A_images 哄酝,B_images 兩個(gè)圖片集合進(jìn)行展示了所灸。由于 OpenCV 無(wú)法在 Notebook 上進(jìn)行圖片的展示。因此我們只能利用 OpenCV 讀取圖片炫七,再利用 Matplotlib 進(jìn)行展示。
import matplotlib.pyplot as plt # plt 用于顯示圖片
figure = np.concatenate([A_images, B_images], axis=0) # 6,256,256,3
figure = figure.reshape((2, 3) + figure.shape[1:]) # 2,3,256,256,3
figure = stack_images(figure) # 512,768,3
%matplotlib inline
# 這里需要指定利用 cv 的調(diào)色板钾唬,否則 plt 展示出來(lái)會(huì)有色差
plt.imshow(cv2.cvtColor(figure, cv2.COLOR_BGR2RGB))
plt.show()
自編碼器
在講解人臉互換所需要的神經(jīng)網(wǎng)絡(luò)之前万哪,讓我們先來(lái)了解一下人臉互換的核心思想:自編碼器。
編碼器與解碼器
自編碼器是一種用于非監(jiān)督學(xué)習(xí)過(guò)程的人工神經(jīng)網(wǎng)絡(luò)抡秆。自動(dòng)編碼器通常由兩部分構(gòu)成:編碼器和解碼器奕巍。下面,我們通過(guò)圖示來(lái)對(duì)其進(jìn)行解釋儒士。
Encoder :編碼器的止,由各種下采樣的方法構(gòu)成。將輸入圖片壓縮成空間特征 (上圖的 Code )着撩,也就是對(duì)原圖片進(jìn)行特征提取诅福。
Decoder :解碼器,由各種上采樣的方法構(gòu)成拖叙。重構(gòu)編碼器輸出的空間特征氓润,并對(duì)其進(jìn)行解碼,輸出新的圖片薯鳍。
自編碼器的全過(guò)程:編碼器對(duì)輸入的圖片進(jìn)行特征提取咖气,然后解碼器對(duì)提取的特征進(jìn)行解析,最后輸出新的圖片挖滤。
從上圖可以看出崩溪,手寫(xiě)的數(shù)字 4 通過(guò)自編碼器后,會(huì)生成一張看起來(lái)像手寫(xiě)字符 4 的新圖片斩松。 那么這樣做有什么意義呢伶唯?其實(shí),在對(duì)圖片進(jìn)行去噪的時(shí)候惧盹,我們經(jīng)常會(huì)采用這種技術(shù)抵怎。
如上圖所示,我們可以將加噪點(diǎn)后的手寫(xiě)字符放入自編碼器中岭参,然后以加噪點(diǎn)前的手寫(xiě)字符為目標(biāo)進(jìn)行訓(xùn)練反惕。最終就能得到一個(gè)專門(mén)處理噪點(diǎn)的神經(jīng)網(wǎng)絡(luò)模型。當(dāng)以后出現(xiàn)新的具有噪點(diǎn)的圖片時(shí)演侯,只需放入訓(xùn)練好的自編碼器就可以直接進(jìn)行去噪了姿染。
根據(jù)上面的知識(shí),我們可以發(fā)現(xiàn)自編碼器最重要的就是編碼器結(jié)構(gòu)和解碼器結(jié)構(gòu)。實(shí)現(xiàn)編碼器的下采樣的方法有很多悬赏,比如我們熟知的池化揭斧、卷積等。但是實(shí)現(xiàn)解碼器的上采樣方法又有哪些呢趟据?怎樣才能將縮小的圖像放大成原圖呢雏掠?這里我們將會(huì)學(xué)習(xí)到一種叫做子像素卷積的上采樣方法。
子像素卷積( Sub-pixel Convolution )
子像素卷積是一種巧妙的圖像及特征圖的 upscale 方法兵多,又叫做 Pixel Shuffle(像素洗牌)尖啡。這種方法于 2016 年被 Wenzhe Shi 等人 提出。較之前的上采樣算法剩膘,子像素卷積在速度和質(zhì)量上都有明顯的提升衅斩。
子像素卷積的結(jié)構(gòu)(主要觀察后面的彩色部分)如下所示:
第一個(gè)彩色部分是通道數(shù)為 r^2,大小為 n×n 的特性圖怠褐,即為 Sub-pixel Convolution 前的圖像畏梆。
第二個(gè)彩色部分是通道數(shù)為 1 ,大小為nr×nr 的特征圖奈懒,即為 Sub-pixel Convolution 后的圖像奠涌。
上圖很直觀得表達(dá)了子像素卷積的做法,前面就是一個(gè)普通的 CNN 網(wǎng)絡(luò)磷杏,到后面彩色部分就是子像素卷積的操作了铣猩。
簡(jiǎn)單的說(shuō),就是將每一個(gè)像素點(diǎn)的所有通道合并在了一起茴丰。例如通道數(shù)為 9达皿,那么我就可以把第一個(gè)像素點(diǎn)的所有通道拿出來(lái),排成一個(gè)3×3 的“像素點(diǎn)”贿肩,如下圖所示:
對(duì)每個(gè)像素點(diǎn)都進(jìn)行上述操作峦椰,最后得到了大小為nr×nr 的特征圖,進(jìn)而提高了原圖的分辨率汰规。這種提高分辨率的過(guò)程就叫做子像素卷積汤功。
因?yàn)?相關(guān)作者 已經(jīng)為我們寫(xiě)好了 Keras 版的子像素卷積函數(shù),所以我們只需要復(fù)制過(guò)來(lái)(無(wú)需手敲)溜哮,直接運(yùn)行即可滔金。以后遇到這種上采樣的需求,也可直接將函數(shù)復(fù)制到本地茂嗓,用以調(diào)用餐茵。子像素卷積的代碼如下:
# 子像素卷積層,用于上采樣
# PixelShuffler layer for Keras
from keras.utils import conv_utils
from keras.engine.topology import Layer
import keras.backend as K
class PixelShuffler(Layer):
# 初始化 子像素卷積層述吸,并在輸入數(shù)據(jù)時(shí)忿族,對(duì)數(shù)據(jù)進(jìn)行標(biāo)準(zhǔn)化處理。
def __init__(self, size=(2, 2), data_format=None, **kwargs):
super(PixelShuffler, self).__init__(**kwargs)
self.data_format = K.normalize_data_format(data_format)
self.size = conv_utils.normalize_tuple(size, 2, 'size')
def call(self, inputs):
# 根據(jù)得到輸入層圖層 batch_size,h 道批,w错英,c 的大小
input_shape = K.int_shape(inputs)
batch_size, h, w, c = input_shape
if batch_size is None:
batch_size = -1
rh, rw = self.size
# 計(jì)算轉(zhuǎn)換后的圖層大小與通道數(shù)
oh, ow = h * rh, w * rw
oc = c // (rh * rw)
# 先將圖層分開(kāi),并且將每一層裝換到自己應(yīng)該到維度
# 最后再利用一次 reshape 函數(shù)(計(jì)算機(jī)會(huì)從外到里的一個(gè)個(gè)的將數(shù)據(jù)排下來(lái))隆豹,這就可以轉(zhuǎn)成指定大小的圖層了
out = K.reshape(inputs, (batch_size, h, w, rh, rw, oc))
out = K.permute_dimensions(out, (0, 1, 3, 2, 4, 5))
out = K.reshape(out, (batch_size, oh, ow, oc))
return out
# compute_output_shape()函數(shù)用來(lái)輸出這一層輸出尺寸的大小
# 尺寸是根據(jù)input_shape以及我們定義的output_shape計(jì)算的椭岩。
def compute_output_shape(self, input_shape):
height = input_shape[1] * self.size[0] if input_shape[1] is not None else None
width = input_shape[2] * self.size[1] if input_shape[2] is not None else None
channels = input_shape[3] // self.size[0] // self.size[1]
return (input_shape[0],
height,
width,
channels)
# 設(shè)置配置文件
def get_config(self):
config = {'size': self.size,
'data_format': self.data_format}
base_config = super(PixelShuffler, self).get_config()
return dict(list(base_config.items()) + list(config.items()))
下采樣層與上采樣層的編寫(xiě)
下采樣和上采樣就是構(gòu)成編碼器和解碼器的具體部件。下采樣層主要用于縮小圖層大小璃赡,擴(kuò)大圖層通道數(shù)(即編碼器)判哥。上采樣主要用于擴(kuò)大圖層大小,縮小圖層通道數(shù)(即解碼器)鉴吹。
在本次實(shí)驗(yàn)中,每個(gè)下采樣層包括了一個(gè)卷積層和一個(gè) LeakyReLU 激活函數(shù)層惩琉。而上采樣包含了一個(gè)卷積層豆励,一個(gè) LeakyReLU 激活函數(shù)層和一個(gè)像素洗牌層。下采樣中的卷積層用于縮小圖層大小提取圖層特征瞒渠,上采樣中的卷積層用于擴(kuò)大圖層通道數(shù)良蒸,保證在像素洗牌后的圖層的通道數(shù)和所需通道數(shù)相同。
假設(shè)在上采樣時(shí)伍玖,我們輸入的圖層大小為32×32 嫩痰,而我們需要輸出的圖層大小為64×64,通道數(shù)為256 窍箍。那么我們就需要在子像素卷積之前先進(jìn)行一次卷積串纺,使得圖層的通道數(shù)變?yōu)?256×4 (即得到 32×32×1024 的圖層)。然后再通過(guò)子像素卷積椰棘,就能夠輸出64×64×256 的新圖層了纺棺。
接下來(lái)我們利用子像素卷積函數(shù)以及 Keras 提供的卷積函數(shù)對(duì)自編碼器中的上采樣層和下采樣層進(jìn)行編寫(xiě)。
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import Conv2D
# 下采樣層,filters 為輸出圖層的通道數(shù)
# n * n * c -> 0.5n * 0.5n * filters
def conv(filters):
def block(x):
# 每一層由一個(gè)使圖層大小減小一半的卷積層和一個(gè) LeakyReLU 激活函數(shù)層構(gòu)成邪狞。
x = Conv2D(filters, kernel_size=5, strides=2, padding='same')(x)
x = LeakyReLU(0.1)(x)
return x
return block
# 上采樣層祷蝌,擴(kuò)大圖層大小
# 圖層的形狀變化如下:
# n*n*c -> n * n * 4filters -> 2n * 2n * filters
def upscale(filters):
# 每一層由一個(gè)擴(kuò)大通道層的卷積,一個(gè)激活函數(shù)和一個(gè)像素洗牌層
def block(x):
# 將通道數(shù)擴(kuò)大為原來(lái)的四倍帆卓。為了下一步能夠通過(guò)像素洗牌 使原來(lái)的圖層擴(kuò)大兩倍
x = Conv2D(filters*4, kernel_size=3, padding='same')(x)
x = LeakyReLU(0.1)(x)
x = PixelShuffler()(x)
return x
return block
接下來(lái)巨朦,我們傳入一張圖片對(duì)上面自定義的兩個(gè)網(wǎng)絡(luò)層進(jìn)行測(cè)試:
import tensorflow as tf
# 將原圖片轉(zhuǎn)為 Tensor 類型
x1 = tf.convert_to_tensor(A_images, dtype=tf.float32)
x2 = conv(126)(x1)
x3 = upscale(3)(x2)
print("將大小為 {} 的圖片傳入 filters 為 126 的下采樣層中得到大小為 {} 的圖層。".format(x1.shape, x2.shape))
print("將大小為 {} 的圖層傳入 filters 為 3 的上采樣層中得到大小為 {} 的圖片剑令。".format(x2.shape, x3.shape))
從結(jié)果可以看出糊啡,上采樣層可以將圖層的大小減小為原來(lái)的 1/2,下采樣層可以將圖層大小擴(kuò)大為原來(lái)的2 倍吁津。
人臉互換的基本架構(gòu)
其實(shí)人臉互換的基本結(jié)構(gòu)就是兩個(gè)自編碼器悔橄,更準(zhǔn)確的說(shuō)應(yīng)該是 1 個(gè)編碼器 + 2 個(gè)解碼器。接下來(lái),我會(huì)從訓(xùn)練過(guò)程和運(yùn)用過(guò)程分別對(duì) AI 換臉的概念進(jìn)行闡述癣疟。
訓(xùn)練過(guò)程
如上圖挣柬,我們利用同一套方法(編碼器)對(duì)兩種圖片進(jìn)行特征提取。將提取出來(lái)的特征放到各自對(duì)應(yīng)的解碼器中睛挚,生成各自所對(duì)應(yīng)的圖像邪蛔。然后利用生成的圖像與原來(lái)的圖像計(jì)算損失,再反向傳播并對(duì)模型參數(shù)進(jìn)行調(diào)整扎狱,如此循環(huán)侧到,直到損失最小。
當(dāng)損失最小時(shí)淤击,我們把川普的圖片放入訓(xùn)練好的(Encoder匠抗,Decoder_A) 中就能夠得到一張和川普神似的圖片。同理污抬,若把凱奇的圖片放入訓(xùn)練好的(Encode汞贸,Decode_B)中,也能得到和凱奇神似的圖片印机。也就是說(shuō)矢腻,在模型訓(xùn)練過(guò)程中,原始圖片既是訓(xùn)練集合也是目標(biāo)集合射赛。
運(yùn)用過(guò)程
現(xiàn)在讓我們理一下思路多柑,A,B 兩類圖片通過(guò)同一種方法進(jìn)行特征提取楣责,然后把得到的特征放入各自的解碼器中得到了屬于自己的圖片竣灌。
也就是說(shuō) Decoder_A 和 Decoder_B 都能夠識(shí)別 Encoder 所提取的特征。因此秆麸,從同一個(gè) Encoder 中出來(lái)的特征既可以放入 Decoder_A 中帐偎,也可以放到 Deconder_B中,這就是人臉互換的關(guān)鍵蛔屹,也是 Encoder 只有一個(gè)的原因削樊。
因此,在上圖的模型訓(xùn)練好后兔毒,我們只需進(jìn)行下圖的操作即可實(shí)現(xiàn)人臉互換:
如上圖所示漫贞,將一張川普的圖片放入訓(xùn)練好的 EnCoder 中,得到一組特征育叁。將這組特征放入 Decoder_A 中迅脐,就能得到一張神似川普的新圖片。若將這組特征放入 Decoder_B 中豪嗽,就會(huì)輸出與川普表情一樣但是和凱奇神似的圖片谴蔑。
神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)
說(shuō)完人臉互換的原理后豌骏,讓我們來(lái)談?wù)劚敬螌?shí)驗(yàn)中用到的 Encoder 和 Decoder 的具體網(wǎng)絡(luò)結(jié)構(gòu),如下圖所示:
中間那層為編碼器的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)隐锭。它由 4 個(gè)下采樣的卷積層窃躲,2 個(gè)全連接層,1 個(gè)上采樣層構(gòu)成钦睡。其中下采樣卷積層用于對(duì)圖片特征進(jìn)行提取蒂窒。全連接層用于打亂特征的空間結(jié)構(gòu),使模型能夠?qū)W習(xí)到更加有用的東西荞怒。上采樣層用于增加圖層大小洒琢。
上下兩層為兩個(gè)解碼器。他們的網(wǎng)絡(luò)結(jié)構(gòu)相同褐桌,但是參數(shù)不同衰抑。他們都是由三個(gè)上采樣層和一個(gè)下采樣卷積層構(gòu)成。其中上采樣層的作用是為了擴(kuò)大圖層大小荧嵌,使最后能夠輸出和原圖片一樣大小的新圖片呛踊。最后的卷積層是為了縮小圖層通道數(shù),使最后輸出的是一個(gè)三通道的圖片完丽。
接下來(lái)利用 Keras 對(duì) Encoder 和 Decoder 進(jìn)行編寫(xiě):
from keras.models import Model
from keras.layers import Input, Dense, Flatten, Reshape
# 定義原圖片的大小
IMAGE_SHAPE = (64, 64, 3)
# 定義全連接的神經(jīng)元個(gè)數(shù)
ENCODER_DIM = 1024
def Encoder():
input_ = Input(shape=IMAGE_SHAPE)
x = input_
x = conv(128)(x)
x = conv(256)(x)
x = conv(512)(x)
x = conv(1024)(x)
x = Dense(ENCODER_DIM)(Flatten()(x))
x = Dense(4*4*1024)(x)
x = Reshape((4, 4, 1024))(x)
x = upscale(512)(x)
return Model(input_, x)
def Decoder():
input_ = Input(shape=(8, 8, 512))
x = input_
x = upscale(256)(x)
x = upscale(128)(x)
x = upscale(64)(x)
x = Conv2D(3, kernel_size=5, padding='same', activation='sigmoid')(x)
return Model(input_, x)
根據(jù)人臉互換所需要的自編碼器結(jié)構(gòu)恋技,創(chuàng)建 (Encoder拇舀,Decoder_A)和(Encoder逻族,Decoder_B)結(jié)構(gòu),并且選擇絕對(duì)平方損失作為模型的損失函數(shù)骄崩。
from tensorflow.keras.optimizers import Adam
# 定義優(yōu)化器
optimizer = Adam(lr=5e-5, beta_1=0.5, beta_2=0.999)
encoder = Encoder()
decoder_A = Decoder()
decoder_B = Decoder()
# 定義輸入函數(shù)大小
x = Input(shape=IMAGE_SHAPE)
# 定義解析 A 類圖片的神經(jīng)網(wǎng)絡(luò)
autoencoder_A = Model(x, decoder_A(encoder(x)))
# 定義解析 B 類圖片的神經(jīng)網(wǎng)絡(luò)
autoencoder_B = Model(x, decoder_B(encoder(x)))
# 使用同一個(gè)優(yōu)化器聘鳞,計(jì)算損失和的最小值。損失函數(shù)采用平均絕對(duì)誤差
autoencoder_A.compile(optimizer=optimizer, loss='mean_absolute_error')
autoencoder_B.compile(optimizer=optimizer, loss='mean_absolute_error')
# 輸出兩個(gè)對(duì)象
autoencoder_A, autoencoder_B
數(shù)據(jù)預(yù)處理
為了能夠訓(xùn)練出較好的模型要拂,在模型訓(xùn)練之前抠璃,我們必須先對(duì)數(shù)據(jù)進(jìn)行相關(guān)處理。接下來(lái)脱惰,我們將對(duì)數(shù)據(jù)集做如下處理:
數(shù)據(jù)增強(qiáng)
數(shù)據(jù)增強(qiáng)是深度學(xué)習(xí)中很重要的一步搏嗡。這種方法可以在不消耗任何成本的情況下,獲得更多的數(shù)據(jù)拉一,進(jìn)而訓(xùn)練出更好的模型采盒。通過(guò)旋轉(zhuǎn)、平移蔚润、縮放磅氨、剪切等操作,將原來(lái)的一張圖片拓展成多張圖片是數(shù)據(jù)增強(qiáng)的一種方法嫡纠。
我們通過(guò)對(duì)旋轉(zhuǎn)角度烦租,平移距離延赌,縮放比例等隨機(jī)取值,來(lái)對(duì)原始圖片進(jìn)行隨機(jī)轉(zhuǎn)換叉橱。
# 該函數(shù)中所有的參數(shù)的值都可以根據(jù)情況自行調(diào)整挫以。
def random_transform(image):
h, w = image.shape[0:2]
# 隨機(jī)初始化旋轉(zhuǎn)角度,范圍 -10 ~ 10 之間赏迟。
rotation = np.random.uniform(-10, 10)
# 隨機(jī)初始化縮放比例屡贺,范圍 0.95 ~ 1.05 之間。
scale = np.random.uniform(0.95, 1.05)
# 隨機(jī)定義平移距離锌杀,平移距離的范圍為 -0.05 ~ 0.05甩栈。
tx = np.random.uniform(-0.05, 0.05) * w
ty = np.random.uniform(-0.05, 0.05) * h
# 定義放射變化矩陣,用于將之前那些變化參數(shù)整合起來(lái)糕再。
mat = cv2.getRotationMatrix2D((w//2, h//2), rotation, scale)
mat[:, 2] += (tx, ty)
# 進(jìn)行放射變化量没,根據(jù)變化矩陣中的變化參數(shù),將圖片一步步的進(jìn)行變化突想,并返回變化后的圖片殴蹄。
result = cv2.warpAffine(
image, mat, (w, h), borderMode=cv2.BORDER_REPLICATE)
# 圖片有 40% 的可能性被翻轉(zhuǎn)
if np.random.random() < 0.4:
result = result[:, ::-1]
return result
讓我們傳入一張圖片,進(jìn)行測(cè)試猾担,并觀察圖片的變化情況:
old_image = A_images[1] # 去之前用于展示的第1張圖片
transform_image = random_transform(old_image)
print("變化前圖片大小為{}\n變化后圖片大小為{}".format(old_image.shape, transform_image.shape))
# 用數(shù)據(jù)可視化部分的函數(shù)進(jìn)行展示
figure = np.concatenate([old_image, transform_image], axis=0)
figure = stack_images(figure)
# 這里需要指定利用 cv 的調(diào)色板袭灯,否則 plt 展示出來(lái)會(huì)有色差
plt.imshow(cv2.cvtColor(figure, cv2.COLOR_BGR2RGB))
仔細(xì)比較結(jié)果中的兩個(gè)川普(從他們的下巴與下邊界的距離,目光方向等方面進(jìn)行比較)绑嘹,你會(huì)發(fā)現(xiàn)圖片已經(jīng)發(fā)生了變化稽荧。當(dāng)然,如果你并沒(méi)有發(fā)現(xiàn)太大變化工腋,可以多次運(yùn)行上述代碼姨丈,如果幸運(yùn)的話,你可以看到圖片出現(xiàn)了翻轉(zhuǎn)(設(shè)置的圖片翻轉(zhuǎn)概率為 40% )擅腰。
輸入數(shù)據(jù)集和目標(biāo)數(shù)據(jù)集
由于圖片已經(jīng)把川普的整個(gè)大頭包含了進(jìn)去蟋恬,而我們需要訓(xùn)練只是川普的臉部特征。因此為了提高模型的訓(xùn)練效率趁冈,我們需要將川普的小臉從他的大頭中切割出來(lái)歼争,即將一張 256×256 的大圖變?yōu)?4×64 的小圖。將切割下來(lái)的小圖放入模型中渗勘,既可以提高模型的訓(xùn)練速度沐绒,又可以提高準(zhǔn)確性。
如果僅僅是裁剪呀邢,我們可以直接進(jìn)行隨機(jī)剪切洒沦。但是為了提高模型的泛化性,在剪切的時(shí)候价淌,我們還需要做一次數(shù)據(jù)增強(qiáng)申眼,也就是將圖片進(jìn)行了扭曲編寫(xiě)瞒津。
那么如何做到圖片的扭曲呢?首先這里我們用到了 OpenCV 中的簡(jiǎn)單映射的方法 :將一張圖片的一個(gè)像素點(diǎn)的值括尸,放到另一張圖片上的某個(gè)像素點(diǎn)上巷蚪。
如上圖所示,為了達(dá)到卷曲的效果濒翻,可以在決定映射位置時(shí)屁柏,添加一個(gè)小的波動(dòng),即某個(gè)點(diǎn)可能會(huì)映射到他原來(lái)位置的相鄰位置有送。比如淌喻,原圖的 4 位置本來(lái)應(yīng)該映射到另外一張圖的 4 位置,但是我們可以加上一個(gè)比較小的隨機(jī)值雀摘,是它的映射位置出現(xiàn)細(xì)微偏移進(jìn)而達(dá)到卷曲的效果裸删。代碼如下(下面代碼會(huì)使用 OpenCV 中的
remap()
函數(shù),不懂的可以查看 該篇博客):
def random_warp(image):
# 先設(shè)置映射矩陣
assert image.shape == (256, 256, 3)
# 設(shè)置 range_ = [ 48., 88., 128., 168., 208.]
range_ = np.linspace(128-80, 128+80, 5)
mapx = np.broadcast_to(range_, (5, 5)) # 利用 Python 廣播的特性將 range_ 復(fù)制 5 份阵赠。
mapy = mapx.T
mapx = mapx + np.random.normal(size=(5, 5), scale=5)
mapy = mapy + np.random.normal(size=(5, 5), scale=5)
# 將大小為 5*5 的map放大為 80*80 涯塔,再進(jìn)行切片,得到 64 * 64 的 map
interp_mapx = cv2.resize(mapx, (80, 80))[8:72, 8:72].astype('float32')
interp_mapy = cv2.resize(mapy, (80, 80))[8:72, 8:72].astype('float32')
# 通過(guò)映射矩陣進(jìn)行剪切和卷曲的操作清蚀,最后獲得 64*64 的訓(xùn)練集圖片
warped_image = cv2.remap(image, interp_mapx, interp_mapy, cv2.INTER_LINEAR)
# 下面四行代碼涉及到 target 的制作匕荸,該段代碼會(huì)在下面進(jìn)行闡述
src_points = np.stack([mapx.ravel(), mapy.ravel()], axis=-1)
dst_points = np.mgrid[0:65:16, 0:65:16].T.reshape(-1, 2)
mat = umeyama(src_points, dst_points, True)[0:2] # umeyama 函數(shù)的定義見(jiàn)下面代碼塊
target_image = cv2.warpAffine(image, mat, (64, 64))
return warped_image, target_image
從上面代碼中可以看出,我們并沒(méi)有直接把做好的輸入數(shù)據(jù)集當(dāng)做目標(biāo)數(shù)據(jù)集枷邪,而是對(duì)輸入數(shù)據(jù)集中的圖片又進(jìn)行了一次轉(zhuǎn)換榛搔。這次轉(zhuǎn)換采用的是點(diǎn)云匹配算法,其本質(zhì)還是一種映射算法齿风。(礙于篇幅本文不作詳解药薯,有興趣的同學(xué)可以查看 該篇論文)绑洛。
當(dāng)然這個(gè)算法的代碼不用我們自己編寫(xiě)救斑,可以直接從官方庫(kù)中下載,我們只需要運(yùn)行一下即可真屯。代碼如下:
# License (Modified BSD)
# umeyama function from scikit-image/skimage/transform/_geometric.py
def umeyama(src, dst, estimate_scale):
"""Estimate N-D similarity transformation with or without scaling.
Parameters
----------
src : (M, N) array
Source coordinates.
dst : (M, N) array
Destination coordinates.
estimate_scale : bool
Whether to estimate scaling factor.
Returns
-------
T : (N + 1, N + 1)
The homogeneous similarity transformation matrix. The matrix contains
NaN values only if the problem is not well-conditioned.
References
----------
.. [1] "Least-squares estimation of transformation parameters between two
point patterns", Shinji Umeyama, PAMI 1991, DOI: 10.1109/34.88573
"""
num = src.shape[0]
dim = src.shape[1]
# Compute mean of src and dst.
src_mean = src.mean(axis=0)
dst_mean = dst.mean(axis=0)
# Subtract mean from src and dst.
src_demean = src - src_mean
dst_demean = dst - dst_mean
# Eq. (38). 下面的Eq 都分別對(duì)應(yīng)著論文中的公式
A = np.dot(dst_demean.T, src_demean) / num
# Eq. (39).
d = np.ones((dim,), dtype=np.double)
if np.linalg.det(A) < 0:
d[dim - 1] = -1
T = np.eye(dim + 1, dtype=np.double)
U, S, V = np.linalg.svd(A)
# Eq. (40) and (43).
rank = np.linalg.matrix_rank(A)
if rank == 0:
return np.nan * T
elif rank == dim - 1:
if np.linalg.det(U) * np.linalg.det(V) > 0:
T[:dim, :dim] = np.dot(U, V)
else:
s = d[dim - 1]
d[dim - 1] = -1
T[:dim, :dim] = np.dot(U, np.dot(np.diag(d), V))
d[dim - 1] = s
else:
T[:dim, :dim] = np.dot(U, np.dot(np.diag(d), V.T))
if estimate_scale:
# Eq. (41) and (42).
scale = 1.0 / src_demean.var(axis=0).sum() * np.dot(S, d)
else:
scale = 1.0
T[:dim, dim] = dst_mean - scale * np.dot(T[:dim, :dim], src_mean.T)
T[:dim, :dim] *= scale
return T
接下來(lái)脸候,我們傳入一張圖片測(cè)試,觀察圖片的變化绑蔫,可以發(fā)現(xiàn)圖片被被裁剪成了64×64×3的新圖片运沦。
warped_image, target_image = random_warp(transform_image) # 返回訓(xùn)練圖片和 target 圖片
print("warpe 前圖片大小{}\nwarpe 后圖片大小{}".format(
transform_image.shape, warped_image.shape))
構(gòu)造 Batch 數(shù)據(jù)集
終于到了數(shù)據(jù)預(yù)處理的最后一步,構(gòu)造 Batch 數(shù)據(jù)集配深。這是深度學(xué)習(xí)中常見(jiàn)的一個(gè)步驟携添,其本質(zhì)就是根據(jù) batch_size 的大小將數(shù)據(jù)集進(jìn)行分批。大小合適的 batch_size 可以使模型更加高效的收斂篓叶。代碼如下:
def get_training_data(images, batch_size):
# 再分批的同時(shí)也把數(shù)據(jù)集打亂烈掠,有序的數(shù)據(jù)集可能使模型學(xué)偏
indices = np.random.randint(len(images), size=batch_size)
for i, index in enumerate(indices):
# 處理該批數(shù)據(jù)集
image = images[index]
# 將圖片進(jìn)行預(yù)處理
image = random_transform(image)
warped_img, target_img = random_warp(image)
# 開(kāi)始分批
if i == 0:
warped_images = np.empty(
(batch_size,) + warped_img.shape, warped_img.dtype)
target_images = np.empty(
(batch_size,) + target_img.shape, warped_img.dtype)
warped_images[i] = warped_img
target_images[i] = target_img
return warped_images, target_images
接下來(lái)我們對(duì)上面代碼進(jìn)行測(cè)試羞秤,從川普的圖片集合中取出一個(gè) batch_size 的數(shù)據(jù)集。
# 加載圖片左敌,并對(duì)圖片進(jìn)行歸一化操作
#注意:由于該段代碼之前 images_A 變量名存的是路徑瘾蛋,而現(xiàn)在存的是真實(shí)的 image 矩陣
#因此如果需要重復(fù)重復(fù)運(yùn)行該段代碼會(huì)報(bào)錯(cuò)(這時(shí)就需要再運(yùn)行一下第一部分的加載圖片路徑的代碼塊)
images_A = load_images(images_A) / 255.0
images_B = load_images(images_B) / 255.0
images_A += images_B.mean(axis=(0, 1, 2)) - images_A.mean(axis=(0, 1, 2))
# 將數(shù)據(jù)進(jìn)行分批,每個(gè)批次 20 條
warped_A, target_A = get_training_data(images_A, 20)
warped_A.shape, target_A.shape
模型訓(xùn)練
現(xiàn)在神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)搭好了矫限,數(shù)據(jù)也準(zhǔn)備齊了哺哼,我們終于可以開(kāi)始進(jìn)行模型的訓(xùn)練了。
# 保存模型
def save_model_weights():
encoder .save_weights("encoder.h5")
decoder_A.save_weights("decoder_A.h5")
decoder_B.save_weights("decoder_B.h5")
print("save model weights")
# 開(kāi)始訓(xùn)練
epochs = 10 # 這里只用作演示叼风,請(qǐng)?jiān)趯?shí)際訓(xùn)練的時(shí)候取董,至少將其調(diào)到 8000 以上
for epoch in range(epochs):
print("第{}代,開(kāi)始訓(xùn)練无宿。甲葬。。".format(epoch))
batch_size = 26
warped_A, target_A = get_training_data(images_A, batch_size)
warped_B, target_B = get_training_data(images_B, batch_size)
loss_A = autoencoder_A.train_on_batch(warped_A, target_A)
loss_B = autoencoder_B.train_on_batch(warped_B, target_B)
print("lossA:{},lossB:{}".format(loss_A, loss_B))
# 下面都為畫(huà)圖和保存模型的操作
save_model_weights()
下圖是我在 Kaggle 上懈贺,利用 GPU 訓(xùn)練了 30 min 中的模型結(jié)果(實(shí)驗(yàn)代碼以及訓(xùn)練后的模型已上傳到 Kaggle 上经窖,點(diǎn)擊這里 可查看)。每類圖片的第一張表示原始圖片梭灿,第二張表示自己的解碼器所生成的圖片画侣,第三張表示對(duì)方的解碼器所生成的圖片。
可以看出堡妒,訓(xùn)練了 30 min 的模型已經(jīng)能夠大概的模仿川普和凱奇的面部輪廓了配乱。
模型運(yùn)用
下載模型,并解壓皮迟。
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1460/models_weights.zip" # 下載數(shù)據(jù)集
!unzip -o "models_weights.zip" # 解壓
整個(gè)換臉模型被保存成了三部分:編碼器 encoder.h5搬泥、解碼器 A decoder_A.h5 和解碼器 B decoder_B.h5。
測(cè)試的代碼和訓(xùn)練代碼雷同伏尼,只是刪去了循環(huán)和訓(xùn)練的步驟忿檩。雖然下列代碼沒(méi)有訓(xùn)練的過(guò)程,但是由于加載模型需要消耗一些時(shí)間爆阶,預(yù)計(jì)運(yùn)行 1~3 min 燥透,請(qǐng)耐心等待。
# 直接加載模型
print("開(kāi)始加載模型辨图,請(qǐng)耐心等待……")
encoder .load_weights("encoder.h5")
decoder_A.load_weights("decoder_A.h5")
decoder_B.load_weights("decoder_B.h5")
# 下面代碼和訓(xùn)練代碼類似
# 獲取圖片班套,并對(duì)圖片進(jìn)行預(yù)處理
images_A = get_image_paths("trump")
images_B = get_image_paths("cage")
# 圖片進(jìn)行歸一化處理
images_A = load_images(images_A) / 255.0
images_B = load_images(images_B) / 255.0
images_A += images_B.mean(axis=(0, 1, 2)) - images_A.mean(axis=(0, 1, 2))
batch_size = 64
warped_A, target_A = get_training_data(images_A, batch_size)
warped_B, target_B = get_training_data(images_B, batch_size)
# 分別取當(dāng)下批次下的川普和凱奇的圖片的前三張進(jìn)行觀察
test_A = target_A[0:3]
test_B = target_B[0:3]
print("開(kāi)始預(yù)測(cè),請(qǐng)耐心等待……")
# 進(jìn)行拼接 原圖 A - 解碼器 A 生成的圖 - 解碼器 B 生成的圖
figure_A = np.stack([
test_A,
autoencoder_A.predict(test_A),
autoencoder_B.predict(test_A),
], axis=1)
# 進(jìn)行拼接 原圖 B - 解碼器 B 生成的圖 - 解碼器 A 生成的圖
figure_B = np.stack([
test_B,
autoencoder_B.predict(test_B),
autoencoder_A.predict(test_B),
], axis=1)
print("開(kāi)始畫(huà)圖故河,請(qǐng)耐心等待……")
# 將多幅圖拼成一幅圖 (已在數(shù)據(jù)可視化部分進(jìn)行了詳細(xì)講解)
figure = np.concatenate([figure_A, figure_B], axis=0)
figure = figure.reshape((2, 3) + figure.shape[1:])
figure = stack_images(figure)
# 將圖片進(jìn)行反歸一化
figure = np.clip(figure * 255, 0, 255).astype('uint8')
# 顯示圖片
plt.imshow(cv2.cvtColor(figure, cv2.COLOR_BGR2RGB))
plt.show()
根據(jù)上述結(jié)果吱韭,可以看出該模型已經(jīng)能夠很好的將川普和凱奇的臉進(jìn)行模仿了。當(dāng)然鱼的,其實(shí)后面還有一些處理工作理盆。比如我們還需要把生成的神似凱奇的臉拼回到原來(lái)的川普的頭上瞻讽。這一步驟涉及到很多圖形學(xué)的知識(shí),比如泊松融合以及 Mask 邊緣融合等熏挎。