1. 程式人生 > >基於Tensorflow的影象風格轉換程式碼

基於Tensorflow的影象風格轉換程式碼

影象風格轉換的概念部分,可以參考部落格:影象風格轉換(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)