基於Tensorflow的影象風格轉換程式碼
阿新 • • 發佈:2018-11-10
影象風格轉換的概念部分,可以參考部落格:影象風格轉換(Image style transfer)
這裡是手動實現了這樣一個demo
import os
import math
import numpy as np
import tensorflow as tf
from PIL import Image
import time
# VGG 自帶的一個常量,之前VGG訓練通過歸一化,所以現在同樣需要作此操作
VGG_MEAN = [103.939, 116.779, 123.68] # rgb 三通道的均值
class VGGNet():
'''
建立 vgg16 網路 結構
從模型中載入引數
'''
def __init__(self, data_dict):
'''
傳入vgg16模型
:param data_dict: vgg16.npy (字典型別)
'''
self.data_dict = data_dict
def get_conv_filter(self, name):
'''
得到對應名稱的卷積層
:param name: 卷積層名稱
:return: 該卷積層輸出
'''
return tf.constant(self.data_dict[name][0], name = 'conv')
def get_fc_weight(self, name):
'''
獲得名字為name的全連線層權重
:param name: 連線層名稱
:return: 該層權重
'''
return tf.constant(self.data_dict[name][0], name = 'fc')
def get_bias(self, name):
'''
獲得名字為name的全連線層偏置
:param name: 連線層名稱
:return: 該層偏置
'''
return tf.constant(self.data_dict[name][1], name = 'bias')
def conv_layer(self, x, name):
'''
建立一個卷積層
:param x:
:param name:
:return:
'''
# 在寫計算圖模型的時候,加一些必要的 name_scope,這是一個比較好的程式設計規範
# 可以防止命名衝突, 二視覺化計算圖的時候比較清楚
with tf.name_scope(name):
# 獲得 w 和 b
conv_w = self.get_conv_filter(name)
conv_b = self.get_bias(name)
# 進行卷積計算
h = tf.nn.conv2d(x, conv_w, strides = [1, 1, 1, 1], padding = 'SAME')
'''
因為此刻的 w 和 b 是從外部傳遞進來,所以使用 tf.nn.conv2d()
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu = None, name = None) 引數說明:
input 輸入的tensor, 格式[batch, height, width, channel]
filter 卷積核 [filter_height, filter_width, in_channels, out_channels]
分別是:卷積核高,卷積核寬,輸入通道數,輸出通道數
strides 步長 卷積時在影象每一維度的步長,長度為4
padding 引數可選擇 “SAME” “VALID”
'''
# 加上偏置
h = tf.nn.bias_add(h, conv_b)
# 使用啟用函式
h = tf.nn.relu(h)
return h
def pooling_layer(self, x, name):
'''
建立池化層
:param x: 輸入的tensor
:param name: 池化層名稱
:return: tensor
'''
return tf.nn.max_pool(x,
ksize = [1, 2, 2, 1], # 核引數, 注意:都是4維
strides = [1, 2, 2, 1],
padding = 'SAME',
name = name
)
def fc_layer(self, x, name, activation = tf.nn.relu):
'''
建立全連線層
:param x: 輸入tensor
:param name: 全連線層名稱
:param activation: 啟用函式名稱
:return: 輸出tensor
'''
with tf.name_scope(name, activation):
# 獲取全連線層的 w 和 b
fc_w = self.get_fc_weight(name)
fc_b = self.get_bias(name)
# 矩陣相乘 計算
h = tf.matmul(x, fc_w)
# 新增偏置
h = tf.nn.bias_add(h, fc_b)
# 因為最後一層是沒有啟用函式relu的,所以在此要做出判斷
if activation is None:
return h
else:
return activation(h)
def flatten_layer(self, x, name):
'''
展平
:param x: input_tensor
:param name:
:return: 二維矩陣
'''
with tf.name_scope(name):
# [batch_size, image_width, image_height, channel]
x_shape = x.get_shape().as_list()
# 計算後三維合併後的大小
dim = 1
for d in x_shape[1:]:
dim *= d
# 形成一個二維矩陣
x = tf.reshape(x, [-1, dim])
return x
def build(self, x_rgb):
'''
建立vgg16 網路
:param x_rgb: [1, 224, 224, 3]
:return:
'''
start_time = time.time()
print('模型開始建立……')
# 將輸入影象進行處理,將每個通道減去均值
r, g, b = tf.split(x_rgb, [1, 1, 1], axis = 3)
'''
tf.split(value, num_or_size_split, axis=0)用法:
value:輸入的Tensor
num_or_size_split:有兩種用法:
1.直接傳入一個整數,代表會被切成幾個張量,切割的維度有axis指定
2.傳入一個向量,向量長度就是被切的份數。傳入向量的好處在於,可以指定每一份有多少元素
axis, 指定從哪一個維度切割
因此,上一句的意思就是從第4維切分,分為3份,每一份只有1個元素
'''
# 將 處理後的通道再次合併起來
x_bgr = tf.concat([b - VGG_MEAN[0], g - VGG_MEAN[1], r - VGG_MEAN[2]], axis = 3)
# assert x_bgr.get_shape().as_list()[1:] == [224, 224, 3]
# 開始構建卷積層
# vgg16 的網路結構
# 第一層:2個卷積層 1個pooling層
# 第二層:2個卷積層 1個pooling層
# 第三層:3個卷積層 1個pooling層
# 第四層:3個卷積層 1個pooling層
# 第五層:3個卷積層 1個pooling層
# 第六層: 全連線
# 第七層: 全連線
# 第八層: 全連線
# 這些變數名稱不能亂取,必須要和vgg16模型保持一致
# 另外,將這些卷積層用self.的形式,方便以後取用方便
self.conv1_1 = self.conv_layer(x_bgr, 'conv1_1')
self.conv1_2 = self.conv_layer(self.conv1_1, 'conv1_2')
self.pool1 = self.pooling_layer(self.conv1_2, 'pool1')
self.conv2_1 = self.conv_layer(self.pool1, 'conv2_1')
self.conv2_2 = self.conv_layer(self.conv2_1, 'conv2_2')
self.pool2 = self.pooling_layer(self.conv2_2, 'pool2')
self.conv3_1 = self.conv_layer(self.pool2, 'conv3_1')
self.conv3_2 = self.conv_layer(self.conv3_1, 'conv3_2')
self.conv3_3 = self.conv_layer(self.conv3_2, 'conv3_3')
self.pool3 = self.pooling_layer(self.conv3_3, 'pool3')
self.conv4_1 = self.conv_layer(self.pool3, 'conv4_1')
self.conv4_2 = self.conv_layer(self.conv4_1, 'conv4_2')
self.conv4_3 = self.conv_layer(self.conv4_2, 'conv4_3')
self.pool4 = self.pooling_layer(self.conv4_3, 'pool4')
self.conv5_1 = self.conv_layer(self.pool4, 'conv5_1')
self.conv5_2 = self.conv_layer(self.conv5_1, 'conv5_2')
self.conv5_3 = self.conv_layer(self.conv5_2, 'conv5_3')
self.pool5 = self.pooling_layer(self.conv5_3, 'pool5')
''' 因為風格轉換隻需要 卷積層 的資料
self.flatten5 = self.flatten_layer(self.pool5, 'flatten')
self.fc6 = self.fc_layer(self.flatten5, 'fc6')
self.fc7 = self.fc_layer(self.fc6, 'fc7')
self.fc8 = self.fc_layer(self.fc7, 'fc8', activation = None)
self.prob = tf.nn.softmax(self.fc8, name = 'prob')
'''
print('建立模型結束:%4ds' % (time.time() - start_time))
# 指定 model 路徑
vgg16_npy_pyth = './vgg16.npy'
# 內容影象 路徑
content_img_path = './shanghai_1.jpg'
# 風格影象路徑
style_img_path = './mosaic_1.jpg'
# 訓練的步數
num_steps = 500
# 指定學習率
learning_rate = 10
# 設定 兩個 引數
lambda_c = 0.1
lambda_s = 500
# 輸入 目錄
output_dir = './run_style_transfer'
if not os.path.exists(output_dir):
os.mkdir(output_dir)
def initial_result(shape, mean, stddev):
'''
定義一個初始化好的隨機圖片,然後在該圖片上不停的梯度下降來得到效果。
:param shape: 輸入形狀
:param mean: 均值
:param stddev: 方法
:return: 圖片
'''
initial = tf.truncated_normal(shape, mean = mean, stddev = stddev) # 一個截斷的正態分佈
'''
tf.truncated_normal(shape, mean, stddev) 生成截斷的生態分佈函式
如果產生的正態分佈值和均值差值大於二倍的標準差,那就重新生成。
'''
return tf.Variable(initial)
def read_img(img_name):
'''
讀取圖片
:param img_name: 圖片路徑
:return: 4維矩陣
'''
img = Image.open(img_name)
# 影象為三通道(224, 244, 3),但是需要轉化為4維
np_img = np.array(img) # 224, 224, 3
np_img = np.asarray([np_img], dtype = np.int32) # 這個函式作用不太理解 (1, 224, 224, 3)
return np_img
def gram_matrix(x):
'''
計算 gram 矩陣
:param x: 特徵圖,shape:[1, width, height, channel]
:return:
'''
b, w, h, ch = x.get_shape().as_list()
# 這裡求出來的是 每一個feature map之間的相似度
features = tf.reshape(x, [b, h * w, ch]) # 將二三維的維度合併,已組成三維
# 相似度矩陣 方法: 將矩陣轉置為[ch, b*w], 再乘原矩陣,最後的矩陣是[ch , ch]
# 防止矩陣數值過大,除以一個常數
gram = tf.matmul(features, features, adjoint_a = True) / tf.constant(ch * w * h, tf.float32) # 引數3, 表示將第一個引數轉置
return gram
# 生成一個影象,均值為127.5,方差為20
result = initial_result((1, 224, 224, 3), 127.5, 20)
# 讀取 內容影象 和 風格影象
content_val = read_img(content_img_path)
style_val = read_img(style_img_path)
content = tf.placeholder(tf.float32, shape = [1, 224, 224, 3])
style = tf.placeholder(tf.float32, shape = [1, 224, 224, 3])
# 載入模型, 注意:在python3中,需要新增一句: encoding='latin1'
data_dict = np.load(vgg16_npy_pyth, encoding='latin1').item()
# 建立這三張影象的 vgg 物件
vgg_for_content = VGGNet(data_dict)
vgg_for_style = VGGNet(data_dict)
vgg_for_result = VGGNet(data_dict)
# 建立 每個 神經網路
vgg_for_content.build(content)
vgg_for_style.build(style)
vgg_for_result.build(result)
# 提取哪些層特徵
# 需要注意的是:內容特徵抽取的層數和結果特徵抽取的層數必須相同
# 風格特徵抽取的層數和結果特徵抽取的層數必須相同
content_features = [vgg_for_content.conv1_2,
vgg_for_content.conv2_2,
# vgg_for_content.conv3_3,
# vgg_for_content.conv4_3,
# vgg_for_content.conv5_3,
]
result_content_features = [vgg_for_result.conv1_2,
vgg_for_result.conv2_2,
# vgg_for_result.conv3_3,
# vgg_for_result.conv4_3,
# vgg_for_result.conv5_3
]
# feature_size, [1, width, height, channel]
style_features = [# vgg_for_style.conv1_2,
# vgg_for_style.conv2_2,
# vgg_for_style.conv3_3,
vgg_for_style.conv4_3,
# vgg_for_style.conv5_3
]
# 為列表中每一個元素,都計算 gram
style_gram = [gram_matrix(feature) for feature in style_features]
result_style_features = [# vgg_for_result.conv1_2,
# vgg_for_result.conv2_2,
# vgg_for_result.conv3_3,
vgg_for_result.conv4_3,
# vgg_for_result.conv5_3
]
result_style_gram = [gram_matrix(feature) for feature in result_style_features]
content_loss = tf.zeros(1, tf.float32)
# 計算內容損失
# 卷積層的形狀 shape:[1, width, height, channel], 需要在三個通道上做平均
for c, c_ in zip(content_features, result_content_features):
content_loss += tf.reduce_mean((c - c_)**2, axis = [1, 2, 3])
# 風格內容損失
style_loss = tf.zeros(1, tf.float32)
for s, s_ in zip(style_gram, result_style_gram):
# 因為在計算gram矩陣的時候,降低了一維,所以,只需要在[1, 2]兩個維度求均值即可
style_loss += tf.reduce_mean( (s - s_)** 2, [1, 2] )
# 總的損失函式
loss = content_loss * lambda_c + style_loss * lambda_s
train_op = tf.train.AdamOptimizer( learning_rate ).minimize(loss)
init_op = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init_op)
for step in range(num_steps):
loss_value, content_loss_value, style_loss_value, _ = \
sess.run([loss, content_loss, style_loss, train_op],
feed_dict = {
content:content_val,
style:style_val
})
# 因為loss_value等,是一個數組,需要通過索引將值去出
print('step: %d, loss_value: %8.4f, content_loss: %8.4f, style_loss: %8.4f' % (step+1,
loss_value[0],
content_loss_value[0],
style_loss_value[0]))
result_img_path = os.path.join(output_dir, 'result_%05d.jpg'%(step+1))
result_val = result.eval(sess)[0] # 將影象取出,因為之前是4維,所以需要使用一個索引0,將其取出
result_val = np.clip(result_val, 0, 255)
# np.clip() numpy.clip(a, a_min, a_max, out=None)[source]
# 其中a是一個數組,後面兩個引數分別表示最小和最大值
img_arr = np.asarray(result_val, np.uint8)
img = Image.fromarray(img_arr)
# 儲存影象
img.save(result_img_path)