基於OpenCV和tensorflow的人臉關鍵點檢測
基於OpenCV和tensorflow的人臉關鍵點檢測
轉載請註明出處https://blog.csdn.net/weixin_40336829/article/details/85456271
引言
最近利用tengine平臺做了一個移植到andriod端的app。本人負責訓練人臉關鍵點檢測的模型。考慮到tengine平臺對tensorflow支援較少,故實現的模型也較為簡單。這裡主要記錄基於tensorflow生成ckpt模型和pb模型的一些方法以及結合opencv在PC端實現實時的人臉關鍵點檢測。
人臉關鍵點資料集
資料集主要使用了kaggle上Facial Keypoints Detection網址: [https://www.kaggle.com/c/facial-keypoints-detection].比賽提供的資料集。該資料集包含包括7,049幅影象,96 x 96畫素的灰度影象。預測15個人臉關鍵點。資料集中每一張圖片剛好包含整個人臉,需要檢測的15個人臉關鍵點如下圖所示。
資料集圖片:
tensorflow模型搭建
這次的人臉關鍵點檢測可以看作是一個迴歸問題。本文使用3層CNN和3層全連線層作為一個baseline。
首先先對資料集進行預處理 預處理程式碼如下
.input_data函式主要是將輸入96*96圖片的畫素值歸一化到[0,1]的區間內,而要預測的15個關鍵點的座標(x,y)也歸一化到[0,1]的區間內。
import pandas as pd
import numpy as np
def input_data(test=False):
file_name = TEST_FILE if test else TRAIN_FILE
df = pd.read_csv(file_name)
cols = df.columns[:-1]
#dropna()是丟棄有缺失資料的樣本,這樣最後7000多個樣本只剩2140個可用的。
df = df.dropna()
df['Image'] = df['Image'].apply(lambda img: np. fromstring(img, sep=' ') / 255.0)
X = np.vstack(df['Image'])
X = X.reshape((-1,96,96,1))
if test:
y = None
else:
y = df[cols].values / 96.0 #將y值縮放到[0,1]區間
return X, y
定義卷積池化操作
import tensorflow as tf
#根據給定的shape定義並初始化卷積核的權值變數
def weight_variable(shape,namew='w'):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial,name=namew)
#根據shape初始化bias變數
def bias_variable(shape,nameb='b'):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial,name=nameb)
#定義卷積操作
def conv2d(x,W):
return tf.nn.conv2d(x,W,strides=[1,1,1,1],padding='VALID')
#定義池化操作
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],strides=[1, 2, 2, 1], padding='SAME')
pb檔案的生成
pb檔案相比於ckpt檔案較為簡化,所有的網路結構及引數均儲存為一個pb檔案。利用tensorflow訓練模型並生成.pb檔案,這裡主要介紹一個簡單的方法來生成。
//生成pb檔案所需要的graph_util庫,用於在pb模型中儲存模型的名稱,方便呼叫pb模型
from tensorflow.python.framework import graph_util
下面為完整的模型訓練及儲存程式碼
import tensorflow as tf
TRAIN_FILE = 'training.csv'
TEST_FILE = 'test.csv'
SAVE_PATH = './model2' #模型儲存路徑
VALIDATION_SIZE = 100 #驗證集大小
EPOCHS = 100 #迭代次數
BATCH_SIZE = 64 #每個batch大小,稍微大一點的batch會更穩定
EARLY_STOP_PATIENCE = 20 #控制early stopping的引數
#儲存模型函式
def save_model(saver,sess,save_path):
path = saver.save(sess, save_path)
print('model save in :{0}'.format(path))
with tf.Session(graph=tf.Graph()) as sess:
#定義的模型,生成pb檔案需要對輸入x和輸出y命名,這裡將輸入x命名為input,輸入y命名為output,同時如果有dropout引數也需進行命名。
x = tf.placeholder("float", shape=[None, 96, 96, 1],name='input')#輸入佔位
y_ = tf.placeholder("float", shape=[None, 30],name='y')#輸出佔位
keep_prob = tf.placeholder("float",name='keep_prob')#dropout概率值
W_conv1 = weight_variable([3, 3, 1, 32],'w1')#32個3*3*1的卷積核
b_conv1 = bias_variable([32],'b1')
h_conv1 = tf.nn.relu(conv2d(x, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
W_conv2 = weight_variable([2, 2, 32, 64],'w2')#64個3*3*32的卷積核
b_conv2 = bias_variable([64],'b2')
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
W_conv3 = weight_variable([2, 2, 64, 128],'w3')#128個3*3*32的卷積核
b_conv3 = bias_variable([128],'b3')
h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3)
h_pool3 = max_pool_2x2(h_conv3)
W_fc1 = weight_variable([11 * 11 * 128, 500],'wf1')#全連線層
b_fc1 = bias_variable([500],'bf1')
h_pool3_flat = tf.reshape(h_pool3, [-1, 11 * 11 * 128])#把第三層卷積的輸出一維向量
h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1)
W_fc2 = weight_variable([500, 500],'wf2')
b_fc2 = bias_variable([500],'bf2')
h_fc2 = tf.nn.relu(tf.matmul(h_fc1, W_fc2) + b_fc2,name='hfc2')
h_fc2_drop = tf.nn.dropout(h_fc2, keep_prob)
W_fc3 = weight_variable([500, 30],'wf3')
b_fc3 = bias_variable([30],'bf3')
y_conv = tf.add(tf.matmul(h_fc2_drop, W_fc3) + b_fc3,0.0,name='output')
#以均方根誤差為代價函式,Adam為
rmse = tf.sqrt(tf.reduce_mean(tf.square(y_ - y_conv)))
train_step = tf.train.AdamOptimizer(1e-3).minimize(rmse)
#變數都要初始化
sess.run(tf.initialize_all_variables())
X,y = input_data()
X_valid, y_valid = X[:VALIDATION_SIZE], y[:VALIDATION_SIZE]
X_train, y_train = X[VALIDATION_SIZE:], y[VALIDATION_SIZE:]
best_validation_loss = 1000000.0
current_epoch = 0
TRAIN_SIZE = X_train.shape[0]
train_index = list(range(TRAIN_SIZE))
np.random.shuffle(train_index)
X_train, y_train = X_train[train_index], y_train[train_index]
saver = tf.train.Saver()
print('begin training..., train dataset size:{0}'.format(TRAIN_SIZE))
for i in range(EPOCHS):
#進行每一輪訓練都需將模型的'input','keep_prob','output'儲存。
constant_graph = graph_util.convert_variables_to_constants(sess, sess.graph_def, ['input','keep_prob','output'])
np.random.shuffle(train_index) #每個epoch都shuffle一下效果更好
X_train, y_train = X_train[train_index], y_train[train_index]
for j in range(0,TRAIN_SIZE,BATCH_SIZE):
print ('epoch {0}, train {1} samples done...'.format(i,j))
train_step.run(feed_dict={x:X_train[j:j+BATCH_SIZE],
y_:y_train[j:j+BATCH_SIZE], keep_prob:0.5})
train_loss = rmse.eval(feed_dict={x:X_train, y_:y_train, keep_prob: 1.0})
validation_loss = rmse.eval(feed_dict={x:X_valid, y_:y_valid, keep_prob: 1.0})
print('epoch {0} done! validation loss:{1}'.format(i, train_loss*96.0))
print('epoch {0} done! validation loss:{1}'.format(i, validation_loss*96.0))
if validation_loss < best_validation_loss:
best_validation_loss = validation_loss
current_epoch = i
#儲存pb檔案。
with tf.gfile.FastGFile(SAVE_PATH+'model.pb', mode='wb') as f: #模型的名字是model.pb
f.write(constant_graph.SerializeToString())
elif (i - current_epoch) >= EARLY_STOP_PATIENCE:
print ('early stopping')
break
從上面的程式碼可以看到在訓練是儲存點.pb模型需要注意3點
(1)對模型的輸入及輸出需要命名,如果有dropout等引數也需命名儲存。
x = tf.placeholder("float", shape=[None, 96, 96, 1],name='input')#輸入命名
keep_prob = tf.placeholder("float",name='keep_prob')#dropout概率值
y_conv = tf.add(tf.matmul(h_fc2_drop, W_fc3) + b_fc3,0.0,name='output')#輸出
(2)在每一次訓練時均需宣告在pb模型需要儲存的命名。即上面的變數
#進行每一輪訓練都需將模型的'input','keep_prob','output'儲存。
constant_graph = graph_util.convert_variables_to_constants(sess, sess.graph_def, ['input','keep_prob','output'])
(3)在儲存模型時執行下列語句
#儲存pb檔案。模型的名字是model.pb
with tf.gfile.FastGFile(SAVE_PATH+'model.pb', mode='wb') as f:
f.write(constant_graph.SerializeToString())
執行上面完整的訓練及儲存程式碼,可以生成.pb模型。
ckpt檔案的生成
生成ckpt檔案,也需要注意幾點。定義模型時需要將模型的每一層進行命名,如上面定義模型時對每一層的命名。同時在將模型儲存.pb檔案的程式碼
#儲存pb檔案。模型的名字是model.pb
with tf.gfile.FastGFile(SAVE_PATH+'model.pb', mode='wb') as f:
f.write(constant_graph.SerializeToString())
改為
#'model/model1.ckpt'為模型儲存路徑及名稱。
save_model(saver,sess,'model/model1.ckpt')
即可。
結合pb檔案和opencv的人臉關鍵點實時檢測
通過上次完整的模型訓練及儲存程式碼,可以生成model2model.pb的檔案,本文將opencv自帶的人臉檢測器haarcascade_frontalface_default.xml,實現圖片中的人臉檢測,框出人臉,並利用model2model.pb對框出的人臉進行特徵點預測
import tensorflow as tf
import cv2
import numpy as np
with tf.Graph().as_default():
output_graph_def = tf.GraphDef()
#開啟.pb檔案
with open('model2model.pb', "rb") as f:
output_graph_def.ParseFromString(f.read())
_ = tf.import_graph_def(output_graph_def, name="")
with tf.Session() as sess:
#讀取.pb檔案中的模型引數
input_x = sess.graph.get_tensor_by_name("input:0")
print(input_x)
keep_prob=sess.graph.get_tensor_by_name("keep_prob:0")
output_y=sess.graph.get_tensor_by_name("output:0")
print(output_y)
#讀取人臉檢測器
face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
#開啟攝像頭
cap=cv2.VideoCapture(0)
while True:
#sess = tf.Session()
ret,img=cap.read()
#檢察攝像頭是否採集到影象
if ret:
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#檢測人臉
faces = face_cascade.detectMultiScale(gray,1.1,5)
#對每一張人臉進行關鍵點檢測
if len(faces)>0:
for faceRect in faces:
x,y,w,h = faceRect
crop=img[x:x+w,y:y+h]
crop = cv2.resize(crop, (96, 96), interpolation=cv2.INTER_CUBIC )
crop=crop/255.0
#關鍵點檢測結果
output_y1= sess.run(output_y, feed_dict={input_x:np.reshape(crop, [-1, 96, 96, 1]),keep_prob:1.0})
pt = np.vstack(np.split(output_y1[0]*96,15)).T
#顯示關鍵點檢測結果
for i in range(15):
cv2.circle(img,(int(pt[0][i]/96*w+x),int(pt[1][i]/96*h+y)),2, (255, 0, 0),-1)
print(crop.shape)
cv2.imshow("img",img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
執行程式可得到下圖的結果
使用.pb檔案需要注意,使用該pb檔案的tensorflow版本必須大於等於訓練時的版本。呼叫時只需要將之前儲存在pb檔案中的input和output等變數取出來,再喂值即可。
CKPT檔案的呼叫
這裡提供一種簡單的方法,對ckpt模型的呼叫不限於此方法。
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
TRAIN_FILE = 'training.csv'
TEST_FILE = 'test.csv'
def input_data(test=False):
file_name = TEST_FILE if test else TRAIN_FILE
df = pd.read_csv(file_name)
cols = df.columns[:-1]
#dropna()是丟棄有缺失資料的樣本,這樣最後7000多個樣本只剩2140個可用的。
df = df.dropna()
df['Image'] = df['Image'].apply(lambda img: np.fromstring(img, sep=' ') / 255.0)
X = np.vstack(df['Image'])
X = X.reshape((-1,96,96,1))
if test:
y =