美顏相機---AI 髮型管家效果的算法解析
####前言
本文爲去年寫的Gitchat文章,由於Gitchat有時間版權限制,一年時間,所以今天才能發佈到CSDN博客上來。
本文爲大家介紹美顏相機中 AI 髮型管家效果的算法解析,當然,本人並沒有美顏相機的算法代碼,只是從自己的角度根據美顏相機的效果呈現,來分析猜測算法流程,最後得到近似的效果。
首先,我們看一下美顏相機的髮型管家介紹:
這個界面明確劃分了男生和女生,我們發現效果呈現流程是這樣的:
###具體流程
1.上傳一張標準正臉測試圖,效果流程如下:
2.上傳一張側臉的測試圖,發現界面提示 “ 使用正臉照片效果更加呦 ~ ”;
3.上傳一張男生測試圖,會得到一個男生髮型效果,女生測試圖會得到女生髮型效果;
4.髮型效果中髮型的顏色可以更改選擇;
5.默認推薦髮型如果用戶不喜歡,可以自行更改;
有了上面的 5 個特點總結,我們就可以分析算法流程了,這裏我分析的結果如下:
####分析算法流程
1.用戶選擇一張照片或者拍照之後,首先進行人臉旋轉角度檢測,如果角度非正臉角度,提示用戶最好重新上傳照片;
2.承接 1 之後,用戶人像照片會進行性別識別等分析,也就是界面 1 中所示的過程,這個過程會做如下處理:
① 性別識別;
② 根據性別到男女對應的髮型模版庫中進行臉型和髮型匹配,得到最接近或者最優的髮型模版匹配;
③ 根據最佳模版的一些參數,對用戶照片進行美顏、美妝處理;
3.根據 2 中得到的最佳模版,將用戶美顏美妝處理之後的照片進行換臉,得到界面 2 中對應的推薦髮型;
4.中得到的效果圖進行頭髮分割,得到對應的頭髮區域;
5.根據 4 中的頭髮區域,對 3 中得到的效果圖進行頭髮換色,得到最終的效果圖;
6.用戶可以手動選擇不同的髮型,然後按照 2 - 5 的步驟得到對應的效果;
好了,到此,我們已經仔細分析了美顏相機發型管家的算法實現原理,當然是猜測的結果,下面我們來驗證一下。
####實現以及過程調整
這裏我將分模塊來實現上述的過程,並將過程做了簡單的調整:
#####**1.人臉檢測 + 關鍵點識別 + 人臉角度檢測**
這一步通常使用第三方人臉 SDK,比如商湯,曠世或者騰訊開源的調用 API 等,當然你也可以自己訓練製作人臉 SDK;
① 判斷有無人臉,有人臉則進行關鍵點檢測和角度檢測;
② 根據角度判斷,正臉範圍則繼續;
#####**2.性別識別**
這裏本人基於最簡單的 CNN 網絡來構架性別識別,具體連接:[性別識別算法博客鏈接](https://blog.csdn.net/trent1985/article/details/80253642)
網絡結構如下:
該網絡中輸入圖片爲大小爲 92 X 112 的人臉單通道灰度圖像,類別標籤(男標籤 [1,0],女標籤 [0,1]),所有參數均在網絡結構圖中標註。
代碼分爲 `GenderUtils.py/GenderTrain.py/GenderTest.py` 三部分
網絡結構部分代碼如下:
# AGE
import matplotlib.image as img
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.python.framework import ops
import math
import os
import csv
def create_placeholders(n_H0, n_W0, n_C0, n_y):
"""
Creates the placeholders for the tensorflow session.
Arguments:
n_H0 -- scalar, height of an input image
n_W0 -- scalar, width of an input image
n_C0 -- scalar, number of channels of the input
n_y -- scalar, number of classes
Returns:
X -- placeholder for the data input, of shape [None, n_H0, n_W0, n_C0] and dtype "float"
Y -- placeholder for the input labels, of shape [None, n_y] and dtype "float"
"""
X = tf.placeholder(name='X', shape=(None, n_H0, n_W0, n_C0), dtype=tf.float32)
Y = tf.placeholder(name='Y', shape=(None, n_y), dtype=tf.float32)
return X, Y
def random_mini_batches(X, Y, mini_batch_size = 64, seed = 0):
"""
Creates a list of random minibatches from (X, Y)
Arguments:
X -- input data, of shape (input size, number of examples) (m, Hi, Wi, Ci)
Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples) (m, n_y)
mini_batch_size - size of the mini-batches, integer
seed -- this is only for the purpose of grading, so that you're "random minibatches are the same as ours.
Returns:
mini_batches -- list of synchronous (mini_batch_X, mini_batch_Y)
"""
m = X.shape[0] # number of training examples
mini_batches = []
np.random.seed(seed)
# Step 1: Shuffle (X, Y)
permutation = list(np.random.permutation(m))
shuffled_X = X[permutation,:,:,:]
shuffled_Y = Y[permutation,:]
# Step 2: Partition (shuffled_X, shuffled_Y). Minus the end case.
num_complete_minibatches = int(math.floor(m / mini_batch_size)) # number of mini batches of size mini_batch_size in your partitionning
for k in range(0, int(num_complete_minibatches)):
mini_batch_X = shuffled_X[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:,:,:]
mini_batch_Y = shuffled_Y[k * mini_batch_size : k * mini_batch_size + mini_batch_size,:]
mini_batch = (mini_batch_X, mini_batch_Y)
mini_batches.append(mini_batch)
# Handling the end case (last mini-batch < mini_batch_size)
if m % mini_batch_size != 0:
mini_batch_X = shuffled_X[num_complete_minibatches * mini_batch_size : m,:,:,:]
mini_batch_Y = shuffled_Y[num_complete_minibatches * mini_batch_size : m,:]
mini_batch = (mini_batch_X, mini_batch_Y)
mini_batches.append(mini_batch)
return mini_batches
def row_csv2dict(csv_file):
dict_club={}
with open(csv_file)as f:
reader=csv.reader(f,delimiter=',')
for row in reader:
dict_club[row[0]]=row[1]
return dict_club
def input_data():
path = "data/train/"
train_num = sum([len(x) for _, _, x in os.walk(os.path.dirname(path))])
image_train = np.zeros((train_num,112,92))
label_train = np.ones((train_num,2))
train_label_dict = row_csv2dict("data/train.csv")
count = 0
for key in train_label_dict:
if int(train_label_dict[key]) == 0:
label_train[count, 0] = 1
label_train[count, 1] = 0
else:
label_train[count, 1] = 1
label_train[count, 0] = 0
filename = path + str(key)
image_train[count] = img.imread(filename)
count = count + 1
path = "data/test/"
test_num = sum([len(x) for _, _, x in os.walk(os.path.dirname(path))])
image_test = np.zeros((test_num, 112,92))
label_test = np.ones((test_num,2))
test_label_dict = row_csv2dict("data/test.csv")
count = 0
for key in test_label_dict:
if int(test_label_dict[key]) == 0:
label_test[count, 0] = 1
label_test[count, 1] = 0
else:
label_test[count, 1] = 1
label_test[count, 0] = 0
filename = path + str(key)
image_test[count] = img.imread(filename)
count = count + 1
return image_train, label_train,image_test, label_test
def weight_variable(shape,name):
return tf.Variable(tf.truncated_normal(shape, stddev = 0.1),name=name)
def bias_variable(shape,name):
return tf.Variable(tf.constant(0.1, shape = shape),name=name)
def conv2d(x,w,padding="SAME"):
if padding=="SAME" :
return tf.nn.conv2d(x, w, strides = [1,1,1,1], padding = "SAME")
else:
return tf.nn.conv2d(x, w, strides = [1,1,1,1], padding = "VALID")
def max_pool(x, kSize, Strides):
return tf.nn.max_pool(x, ksize = [1,kSize,kSize,1],strides = [1,Strides,Strides,1], padding = "SAME")
def compute_cost(Z3, Y):
"""
Computes the cost
Arguments:
Z3 -- output of forward propagation (output of the last LINEAR unit), of shape (6, number of examples)
Y -- "true" labels vector placeholder, same shape as Z3
Returns:
cost - Tensor of the cost function
"""
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=Z3, labels=Y))
return cost
def initialize_parameters():
tf.set_random_seed(1)
W1 = tf.cast(weight_variable([5,5,1,32],"W1"), dtype = tf.float32)
b1 = tf.cast(bias_variable([32],"b1"), dtype = tf.float32)
W2 = tf.cast(weight_variable([5,5,32,64],"W2"), dtype = tf.float32)
b2 = tf.cast(bias_variable([64],"b2"), dtype = tf.float32)
W3 = tf.cast(weight_variable([5,5,64,128],"W3"), dtype = tf.float32)
b3 = tf.cast(bias_variable([128],"b3"), dtype = tf.float32)
W4 = tf.cast(weight_variable([14*12*128,500],"W4"), dtype = tf.float32)
b4 = tf.cast(bias_variable([500],"b4"), dtype = tf.float32)
W5 = tf.cast(weight_variable([500,500],"W5"), dtype = tf.float32)
b5 = tf.cast(bias_variable([500],"b5"), dtype = tf.float32)
W6 = tf.cast(weight_variable([500,2],"W6"), dtype = tf.float32)
b6 = tf.cast(bias_variable([2],"b6"), dtype = tf.float32)
parameters = {"W1":W1,
"b1":b1,
"W2":W2,
"b2":b2,
"W3":W3,
"b3":b3,
"W4":W4,
"b4":b4,
"W5":W5,
"b5":b5,
"W6":W6,
"b6":b6}
return parameters
def cnn_net(x, parameters, keep_prob = 1.0):
#frist convolution layer
w_conv1 = parameters["W1"]
b_conv1 = parameters["b1"]
h_conv1 = tf.nn.relu(conv2d(x,w_conv1) + b_conv1) #output size 112x92x32
h_pool1 = max_pool(h_conv1,2,2) #output size 56x46x32
#second convolution layer
w_conv2 = parameters["W2"]
b_conv2 = parameters["b2"]
h_conv2 = tf.nn.relu(conv2d(h_pool1, w_conv2) + b_conv2) #output size 56x46x64
h_pool2 = max_pool(h_conv2,2,2) #output size 28x23x64
#third convolution layer
w_conv3 = parameters["W3"]
b_conv3 = parameters["b3"]
h_conv3 = tf.nn.relu(conv2d(h_pool2,w_conv3) + b_conv3) #output size 28x23x128
h_pool3 = max_pool(h_conv3,2,2) #output size 14x12x128
#full convolution layer
w_fc1 = parameters["W4"]
b_fc1 = parameters["b4"]
h_fc11 = tf.reshape(h_pool3,[-1,14*12*128])
h_fc1 = tf.nn.relu(tf.matmul(h_fc11,w_fc1) + b_fc1)
w_fc2 = parameters["W5"]
b_fc2 = parameters["b5"]
h_fc2 = tf.nn.relu(tf.matmul(h_fc1,w_fc2)+b_fc2)
h_fc2_drop = tf.nn.dropout(h_fc2,keep_prob)
w_fc3 = parameters["W6"]
b_fc3 = parameters["b6"]
y_conv = tf.matmul(h_fc2_drop, w_fc3) + b_fc3
#y_conv = tf.nn.softmax(tf.matmul(h_fc2_drop, w_fc3) + b_fc3)
#rmse = tf.sqrt(tf.reduce_mean(tf.square(y_ - y_conv)))
#cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels = y, logits = y_conv))
#train_step = tf.train.GradientDescentOptimizer(0.001).minimize(cross_entropy)
#correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y,1))
#accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
return y_conv
def save_model(saver,sess,save_path):
path = saver.save(sess, save_path)
print 'model save in :{0}'.format(path)
完整代碼工程下載連接:
百度網盤地址(密碼5wst):[百度網盤下載地址]
Github地址:[Github下載地址]
#####**3.臉型與髮型匹配**
在該模塊中,本人使用了最簡單的歐式距離,來進行用戶照片與模版臉型的匹配計算:
假設用戶臉型點位(人臉關鍵點)爲A,模版關鍵點爲B,遍歷模版庫中所有模版,計算距離 Dis:
>Dis =sqrt( (Ax - Bx) * (Ax - Bx) + (Ay - By) * (Ay - By))
選取 Dis 最小的模版作爲最優模版;
上述過程是臉型的匹配,根據美顏相機的提示,只顯示了臉型分析,這裏本人猜測還有對於髮型的匹配,比如,短髮型的用戶照片,會匹配到短髮型的模版效果,不過本人暫時沒有找到合適的算法,這裏暫時忽略;
#####**4. 根據 3 中得到的最優模版,設定該模版對應的美顏和美妝參數,對用戶照片進行美顏美妝處理;**
這個過程中,美顏包括磨皮美白、大眼、瘦臉等等,看大傢俱體需求而定,我這裏只進行了磨皮美白美顏算法,這裏相關連接如下:
>[磨皮算法博客 A]
[磨皮算法博客 B]
磨皮美白算法相關的資料很多,大家也可以自行百度,本人這裏主要講 AI 髮型管家的算法流程問題。
#####**爲什麼這裏不是統一固定的美顏美妝參數呢?**
原因是這樣的:不同的髮型效果圖也是不一樣的,考慮到不同髮型,不同顏色場景,實際上從審美角度看,是需要搭配不同的服飾,不同的妝容的,所以這裏是不同模版對應不同的參數。
美顏之後是化妝,這裏本人使用的是妝容遷移技術,直接將妝容模版的妝容效果遷移到用戶照片中去,相關的算法、效果連接如下:
這一步對應的效果圖舉例:
這裏本人直接使用美顏相機發型管家處理的效果圖當作本人的模版圖(模版的設計非常考究,需要設計高手+有版權的模特,本人這裏測試圖僅供測試,切勿做商務用途,以免侵權,若有侵權敬請告知,本人立刻刪除),如下所示:
根據上面的換臉模版圖,我們進行妝容遷移 + 換臉,如下圖所示:
#####**6.頭髮換色**
這個模塊主要實現最後效果圖的頭髮顏色更改,用戶可以有多種顏色選擇,來滿足自己的審美需求;
換髮色的算法基本上是基於顏色空間的顏色替換,具體流程如下:
本人做過詳細的算法講解,連接如下:[染髮算法博客鏈接]
這一步的算法效果舉例如下:
上面 1 - 6 個步驟,就是本人實現的關於美顏相機發型管家效果的算法流程,由於這個流程中相關的算法太複雜,模塊組合太多,所以基本上本人以算法流程解析爲主,相關的算法具體實現都給了對應的算法與 demo 的連接,大家可以仔細研究,這裏不要吐槽算法實現的繁瑣,實際上任何一個好的效果,它的背後大多數情況下都是多個算法的組合;
最後給出本人實現的完整的效果流程圖:
對比美顏相機發型管家的效果如下:
最後本人給出一個 DEMO 看效果:[美顏相機發型管家 PC DEMO ]
>注意:算法是核心,掌握了算法流程,纔是真正的掌握,不要過分追求代碼,沒意義。(不少讀者吐槽我不分享代碼,一方面本人理論上只講思路,另一方面,研究可能涉及商業機密,不方便給出詳細代碼,再者,一味追求代碼複製粘貼的圖像算法工程師絕對不是一個合格的圖像算法工程師!)