參加學校的國創比賽的時候,我們小組的項目有一部分內容需要用到利用攝像頭實現實時檢測人臉的表情,因為最近都在看深度學習方面的相關知識,所以就自己動手實現了一下這個小Demo.參考網上的資料,發現大部分是使用CNN和DNN進行學習,經過本人親自實踐,我發現DNN的識別效果更佳~(樓主接下來就要講的是基於DNN的模型,要是你對CNN的模型感興趣,歡迎私戳樓主~)
所需環境:opencv + tensorflow1.8 + pycharm
代碼以及模型的下載地址:https://github.com/tgpcai/Microexpression_recognition(如果喜歡請幫樓主點個start~)
最后實現的結果圖:
目錄
1.數據集簡介
2.代碼實現
(0)實現Demo的代碼組織結構
(1)數據預處理
(2)訓練模型
(3)調用模型實現人臉微表情識別
3.個人遇坑以及總結
1.數據集簡介
人臉表情識別程序所采用的數據集為FER2013(facial-expression-recognition 2013)以及中科院的微表情數據,該數據集需要在kaggle網
站上進行下載,下載地址為:https://www.kaggle.com/c/challenges-in-representation-learning-facial-expression-recognition-challenge/data
下載的數據是壓縮文件,對其進行手動解壓,解壓后文件格式是csv,文件名為fer2013.csv,對這個文件進行提取可以得到三個文件,具體的提取過程會在后續進行介紹:
其中train文件的emotion一共有7種,csv文件中每個數字對應一種表情,7種表情包括:0 - 'angry', 1 - 'disgusted',2 - 'fearful', 3 - 'happy', 4 - 'sad', 5 - 'surprised', 6 - 'neutral'(這也是咱們能識別的表情)
打開train.csv文件每一行除了emotion的標記之外,還有一幅影像,只不過在csv文件里,影像用灰度值表示了,csv文件打開之后,里面的內容如下:
不過可能由於FER2013數據集的質量不太好,大部分模型的驗證精度只有60%(如googleNet和AlexNet都只有63%)左右,好一點的可以到70%。我自己訓練的模型驗證經度只有55-56左右!
網上很多參考資料都把灰度值表示的影像變成了.jpg能看的圖片,但實際上並不需要這樣做,因為你把這灰度值的影像變為.jpg,后面還是需要經過一系列步驟在轉換成灰度值,不過我也會貼出如何把這灰度值影響變為.jpg結尾的圖片!
中科院的數據集如下:
同樣對其進行灰度化、圖像大小的處理,之后方可當成輸入數據進行模型的訓練。
2.代碼實現
(0)實現Demo的代碼組織結構
demo.py 調用系統攝像頭完成實時識別人臉微表情
main.py 包含訓練模型、測試模型的接口
model.py DNN算法的實現
utils.py 對數據集合的預處理
(1)數據預處理
有兩種方式,第一種方式只是為了看看數據集的圖片,而不是只能看到灰度值,而真正參與模型的訓練的數據處理方式是第二種!
1)把這灰度值的影像變為.jpg
1 import csv 2 import os 3 4 #數據集路徑 5 database_path = 'C:/Users/tgp/Desktop/Machine Learning/人臉識別/fer2013' 6 7 #將路徑組合后返回 8 csv_file = os.path.join(database_path, 'fer2013.csv') 9 train_csv = os.path.join(database_path, 'train.csv') 10 val_csv = os.path.join(database_path, 'val.csv') 11 test_csv = os.path.join(database_path, 'test.csv') 12 13 with open(csv_file) as f: 14 #使用csv中的reader()打開.csv文件 15 csvr = csv.reader(f) 16 17 #將迭代器指向文件的第二行,因為第一行為標簽 18 header = next(csvr) 19 rows = [row for row in csvr] 20 21 #按最后一列的標簽將數據集進行分割 22 trn = [row[:-1] for row in rows if row[-1] == 'Training'] 23 csv.writer(open(train_csv, 'w+'), lineterminator='\n').writerows([header[:-1]] + trn) 24 print(len(trn)) 25 26 val = [row[:-1] for row in rows if row[-1] == 'PublicTest'] 27 csv.writer(open(val_csv, 'w+'), lineterminator='\n').writerows([header[:-1]] + val) 28 print(len(val)) 29 30 tst = [row[:-1] for row in rows if row[-1] == 'PrivateTest'] 31 csv.writer(open(test_csv, 'w+'), lineterminator='\n').writerows([header[:-1]] + tst) 32 print(len(tst))
1 import csv 2 import os 3 from PIL import Image 4 import numpy as np 5 6 database_path = 'C:/Users/tgp/Desktop/Machine Learning/人臉識別/fer2013' 7 8 train_csv = os.path.join(database_path, 'train.csv') 9 val_csv = os.path.join(database_path, 'val.csv') 10 test_csv = os.path.join(database_path, 'test.csv') 11 12 train_set = os.path.join(database_path, 'train') 13 val_set = os.path.join(database_path, 'val') 14 test_set = os.path.join(database_path, 'test') 15 16 for save_path, csv_file in [(train_set, train_csv), (val_set, val_csv), (test_set, test_csv)]: 17 if not os.path.exists(save_path): 18 os.makedirs(save_path) 19 20 num = 1 21 with open(csv_file) as f: 22 csvr = csv.reader(f) 23 header = next(csvr) 24 #使用enumerate遍歷csvr中的標簽(label)和特征值(pixel) 25 for i, (label, pixel) in enumerate(csvr): 26 #將特征值的數組轉化為48*48的矩陣 27 pixel = np.asarray([float(p) for p in pixel.split()]).reshape(48, 48) 28 subfolder = os.path.join(save_path, label) 29 if not os.path.exists(subfolder): 30 os.makedirs(subfolder) 31 #將該矩陣轉化為RGB圖像,再通過convert轉化為8位灰度圖像,L指灰度圖模式,L=R*299/1000+G*587/1000+B*114/1000 32 im = Image.fromarray(pixel).convert('L') 33 image_name = os.path.join(subfolder, '{:05d}.jpg'.format(i)) 34 print(image_name) 35 im.save(image_name)
執行完上面的代碼,你就可以在相應的路徑看到如下圖:
這樣就完成了從灰度圖到可視化的轉變。
2)簡便的數據預處理(訓練模型時采用的辦法)
1 import collections 2 import numpy as np 3 import pandas as pd 4 from tensorflow.python.framework import dtypes, random_seed 5 6 7 def load_data(data_file): 8 data = pd.read_csv(data_file) 9 pixels = data['pixels'].tolist() 10 width = 48 11 height = 48 12 faces = [] 13 for pixel_sequence in pixels: 14 # 從csv中獲取人臉的數據 15 face = [int(pixel) for pixel in pixel_sequence.split(' ')] 16 # 把臉的數據變為48*48像素,利用plt.imshow即可打印出圖片 17 face = np.asarray(face).reshape(width, height) 18 faces.append(face) 19 # 把faces從列表變為三維矩陣。(35887,)----->(35887,48,48) 20 faces = np.asarray(faces) 21 # 添加維度,將faces從(35887,48,48)------>(35887,48,48,1) 22 faces = np.expand_dims(faces, -1) 23 # one-hot編碼,把屬於該類表情置1,其余為0,並轉換為矩陣 24 emotions = pd.get_dummies(data['emotion']).as_matrix() 25 return faces, emotions 26 27 28 class DataSet(object): 29 def __init__(self, images, labels, reshape=True, dtype=dtypes.float32, seed=None): 30 seed1, seed2 = random_seed.get_seed(seed) 31 np.random.seed(seed1 if seed is None else seed2) 32 if reshape: 33 # 將images(35887,48,48,1)變為(35887,2304) 34 assert images.shape[3] == 1 35 images = images.reshape(images.shape[0],images.shape[1]*images.shape[2]) 36 37 # 類型轉換,並進行灰度處理 38 if dtype == dtypes.float32: 39 images = images.astype(np.float32) 40 images = np.multiply(images, 1.0 / 255.0) 41 # 設置私有屬性 42 self._num_examples = images.shape[0] 43 self._images = images 44 self._labels = labels 45 self._epochs_completed = 0 46 self._index_in_epoch = 0 47 48 @property 49 def images(self): 50 return self._images 51 52 @property 53 def labels(self): 54 return self._labels 55 56 @property 57 def num_examples(self): 58 return self.num_examples 59 60 @property 61 def epochs_completed(self): 62 self._epochs_completed 63 64 # 批量獲取訓練數據 65 def next_batch(self, batch_size,shuffle=True): 66 start = self._index_in_epoch 67 if self._epochs_completed == 0 and start == 0 and shuffle: 68 # 打亂順序 69 perm0 = np.arange(self._num_examples) 70 np.random.shuffle(perm0) 71 self._images = self._images[perm0] 72 self._labels = self._labels[perm0] 73 74 if start + batch_size > self._num_examples: 75 self._epochs_completed += 1 76 rest_num_examples = self._num_examples - start 77 images_rest_part = self._images[start:self._num_examples] 78 labels_rest_part = self._labels[start:self._num_examples] 79 # 當剩余的數據不夠一次batch_size,就在之前的數據中隨機選取並進行組合 80 if shuffle: 81 perm = np.arange(self._num_examples) 82 np.random.shuffle(perm) 83 self._images = self._images[perm] 84 self._labels = self._labels[perm] 85 start = 0 86 self._index_in_epoch = batch_size - rest_num_examples 87 end = self._index_in_epoch 88 images_new_part = self._images[start:end] 89 labels_new_part = self._labels[start:end] 90 return np.concatenate((images_rest_part, images_new_part), axis=0), np.concatenate( 91 (labels_rest_part, labels_new_part), axis=0) 92 else: 93 self._index_in_epoch += batch_size 94 end = self._index_in_epoch 95 return self._images[start:end], self._labels[start:end] 96 97 98 def input_data(train_dir, dtype = dtypes.float32, reshape = True, seed=None): 99 training_size = 28709 100 validation_size = 3589 101 test_size = 3589 102 103 train_faces, train_emotions = load_data(train_dir) 104 print("Data load success!") 105 106 # 驗證數據 107 validation_faces = train_faces[training_size: training_size + validation_size] 108 validation_emotions = train_emotions[training_size: training_size + validation_size] 109 110 # 測試數據 111 test_faces = train_faces[training_size + validation_size:] 112 test_emotions = train_emotions[training_size + validation_size:] 113 114 # 訓練數據 115 train_faces = train_faces[: training_size] 116 train_emotions = train_emotions[: training_size] 117 118 Datasets = collections.namedtuple('Datasets', ['train', 'validation', 'test']) 119 train = DataSet(train_faces, train_emotions, reshape=reshape,) 120 validation = DataSet(validation_faces, validation_emotions, dtype=dtype, reshape=reshape, seed=seed) 121 test = DataSet(test_faces, test_emotions, dtype=dtype, reshape=reshape, seed=seed) 122 return Datasets(train=train, validation=validation, test=test)
到此,我們就完成了對fer2013數據的處理。
(2)訓練模型
采用的是DNN模型,個人感覺DNN的大致過程和CNN較為相似,樓主有一篇博客就是利用CNN實現手寫數字的識別,感興趣或者對於DNN實現算法不是很理解的,可以跳過去看看。
1 import os 2 import cv2 3 import tensorflow as tf 4 from utils import * 5 6 EMOTIONS = ['angry', 'disgusted', 'fearful', 'happy', 'sad', 'surprised', 'neutral'] 7 8 9 def deepnn(x): 10 x_image = tf.reshape(x, [-1, 48, 48, 1]) 11 # conv1 12 W_conv1 = weight_variables([5, 5, 1, 64]) 13 b_conv1 = bias_variable([64]) 14 h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) 15 # pool1 16 h_pool1 = maxpool(h_conv1) 17 # norm1 18 norm1 = tf.nn.lrn(h_pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75) 19 20 # conv2 21 W_conv2 = weight_variables([3, 3, 64, 64]) 22 b_conv2 = bias_variable([64]) 23 h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) 24 norm2 = tf.nn.lrn(h_conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75) 25 h_pool2 = maxpool(norm2) 26 27 # Fully connected layer 28 W_fc1 = weight_variables([12 * 12 * 64, 384]) 29 b_fc1 = bias_variable([384]) 30 h_conv3_flat = tf.reshape(h_pool2, [-1, 12 * 12 * 64]) 31 h_fc1 = tf.nn.relu(tf.matmul(h_conv3_flat, W_fc1) + b_fc1) 32 33 # Fully connected layer 34 W_fc2 = weight_variables([384, 192]) 35 b_fc2 = bias_variable([192]) 36 h_fc2 = tf.matmul(h_fc1, W_fc2) + b_fc2 37 38 # linear 39 W_fc3 = weight_variables([192, 7]) 40 b_fc3 = bias_variable([7]) 41 y_conv = tf.add(tf.matmul(h_fc2, W_fc3), b_fc3) 42 43 return y_conv 44 45 46 def conv2d(x, W): 47 return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') 48 49 50 def maxpool(x): 51 return tf.nn.max_pool(x, ksize=[1, 3, 3, 1], 52 strides=[1, 2, 2, 1], padding='SAME') 53 54 55 def weight_variables(shape): 56 initial = tf.truncated_normal(shape, stddev=0.1) 57 return tf.Variable(initial) 58 59 60 def bias_variable(shape): 61 initial = tf.constant(0.1, shape=shape) 62 return tf.Variable(initial) 63 64 65 def train_model(train_data): 66 fer2013 = input_data(train_data) 67 max_train_steps = 30001 68 69 x = tf.placeholder(tf.float32, [None, 2304]) 70 y_ = tf.placeholder(tf.float32, [None, 7]) 71 72 y_conv = deepnn(x) 73 74 cross_entropy = tf.reduce_mean( 75 tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv)) 76 train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy) 77 correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1)) 78 accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) 79 80 with tf.Session() as sess: 81 saver = tf.train.Saver() 82 sess.run(tf.global_variables_initializer()) 83 84 for step in range(max_train_steps): 85 batch = fer2013.train.next_batch(25) 86 if step % 100 == 0: 87 train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1]}) 88 print('step %d, training accuracy %g' % (step, train_accuracy)) 89 if step + 1 == max_train_steps: 90 saver.save(sess, './models/emotion_model', global_step=step + 1) 91 train_step.run(feed_dict={x: batch[0], y_: batch[1]}) 92 93 94 def image_to_tensor(image): 95 tensor = np.asarray(image).reshape(-1, 2304) * 1 / 255.0 96 return tensor
訓練完模型后在你相應的文件夾里面會出現如下圖所示:
這就是訓練模型保存的參數,樓主最后也會貼出自己訓練的模型,方便你們下載和使用。
(3)調用模型實現人臉微表情識別
采用的方法是調用攝像頭實時識別人臉微表情,具體的過程為:調用opencv自帶的人臉識別器,調用系統攝像頭拍攝出人臉圖片,對人臉圖片進行預處理,將處理完成的圖片傳入模型,最后將模型分析的結果反饋至運行的窗口
如下圖所示:這些都是opencv自帶的有關臉部識別的識別器。
一般采用的是這幾個:
其中個人認為_alt2的識別器效果最好。
具體如何調用系統攝像頭以及實時別人微表情的代碼如下:
1 import cv2 2 import numpy as np 3 import sys 4 import tensorflow as tf 5 import PIL.Image as Image 6 import matplotlib.pyplot as plt 7 from model import * 8 9 # 加載opencv自帶的人臉識別器 10 CASC_PATH = 'D:/Anaconda3/Lib/site-packages/cv2/data/haarcascade_frontalface_alt2.xml' 11 cascade_classifier = cv2.CascadeClassifier(CASC_PATH) 12 # 人臉七種微表情 13 EMOTIONS = ['angry', 'disgusted', 'fearful', 'happy', 'sad', 'surprised', 'neutral'] 14 15 16 def format_image(image): 17 # image如果為彩色圖:image.shape[0][1][2](水平、垂直像素、通道數) 18 if len(image.shape) > 2 and image.shape[2] == 3: 19 # 將圖片變為灰度圖 20 image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 21 # 它可以檢測出圖片中所有的人臉,並將人臉用vector保存各個人臉的坐標、大小(用矩形表示) 22 # 調整scaleFactor參數的大小,可以增加識別的靈敏度,推薦1.1 23 faces = cascade_classifier.detectMultiScale(image, scaleFactor=1.1, minNeighbors=5) 24 # 如果圖片中沒有檢測到人臉,則返回None 25 if not len(faces) > 0: 26 return None, None 27 # max_are_face包含了人臉的坐標,大小 28 max_are_face = faces[0] 29 # 在所有人臉中選一張最大的臉 30 for face in faces: 31 if face[2] * face[3] > max_are_face[2] * max_are_face[3]: 32 max_are_face = face 33 34 # 這兩步可有可無 35 face_coor = max_are_face 36 image = image[face_coor[1]:(face_coor[1] + face_coor[2]), face_coor[0]:(face_coor[0] + face_coor[3])] 37 # 調整圖片大小,變為48*48 38 try: 39 image = cv2.resize(image, (48, 48), interpolation=cv2.INTER_CUBIC) 40 except Exception: 41 print("problem during resize") 42 return None, None 43 44 return image, face_coor 45 46 47 def demo(modelPath, showBox=True): 48 # 調用模型分析人臉微表情 49 # tf.reset_default_graph() 50 face_x = tf.placeholder(tf.float32, [None, 2304]) 51 y_conv = deepnn(face_x) 52 probs = tf.nn.softmax(y_conv) 53 54 # 加載模型 55 saver = tf.train.Saver() 56 ckpt = tf.train.get_checkpoint_state(modelPath) 57 sess = tf.Session() 58 if ckpt and ckpt.model_checkpoint_path: 59 saver.restore(sess, ckpt.model_checkpoint_path) 60 print("Restore model sucsses!!\nNOTE: Press 'a' on keyboard to capture face.") 61 62 # feelings_facesy用來存儲emojis表情 63 feelings_faces = [] 64 for index, emotion in enumerate(EMOTIONS): 65 # imread函數(文件路徑,讀取方式) 66 # cv2.IMREAD_COLOR:讀入一副彩色圖片;(1)返回三維矩陣,且為[120,120,3] 67 # cv2.IMREAD_GRAYSCALE:以灰度模式讀入圖片;(0)返回二維矩陣,且為[120,120] 68 # cv2.IMREAD_UNCHANGED:讀入一幅圖片,並包括其alpha通道(-1)返回三維矩陣,且為[120,120,4] 69 feelings_faces.append(cv2.imread('D:/Dict/Facial-Expression-Recognition-master/data/emojis/' + emotion + '.png', 1)) 70 71 # 獲取筆記本的攝像頭, 72 video_captor = cv2.VideoCapture(0) 73 74 emoji_face = [] 75 result = None 76 while True: 77 # 獲取攝像頭的每幀圖片,若獲得,則ret的值為True,frame就是每一幀的圖像,是個三維矩陣 78 ret, frame = video_captor.read() 79 80 detected_face, face_coor = format_image(frame) 81 if showBox: 82 if face_coor is not None: 83 # 獲取人臉的坐標,並用矩形框出 84 [x, y, w, h] = face_coor 85 cv2.rectangle(frame, (x, y), (x + w, y + h), (255, 255, 0), 2) 86 87 # 每隔10ms刷新一次,並且等當鍵盤輸入a的時候,截取圖像,因為是64位系統所以必須要0xFF == ord('a') 88 if cv2.waitKey(1) & 0xFF == ord('a'): 89 if detected_face is not None: 90 cv2.imwrite('a.jpg', detected_face) 91 print(detected_face) 92 print("獲取成功") 93 # 將圖片變為tensorflow可以接受的格式 94 tensor = image_to_tensor(detected_face) 95 result = sess.run(probs, feed_dict={face_x: tensor}) 96 print(result) 97 98 if result is not None: 99 for index, emotion in enumerate(EMOTIONS): 100 # 將七種微表情的文字添加到圖片中 101 cv2.putText(frame,emotion,(10,index*20 + 20),cv2.FONT_HERSHEY_PLAIN, 1, (0, 255, 0), 1) 102 # 將七種微表情的概率用矩形表現出來 103 cv2.rectangle(frame,(130, index*20 + 10),(130+int(result[0][index]*100), (index + 1) * 20 + 4), (255, 0, 0), -1) 104 # 獲取人臉微表情相應的emojis表情 105 emoji_face = feelings_faces[np.argmax(result[0])] 106 107 # 將emojis表情添加到圖片中的指定位置 方法1: 108 frame[200:320, 10:130, :] = emoji_face[:, :, :] 109 cv2.imwrite('b.jpg', frame) 110 # 將emojis表情添加到圖片中的指定位置 方法2: 111 # for c in range(0, 1): 112 # frame[200:320, 10:130, c] = emoji_face[:, :, c] * (emoji_face[:, :, 3] / 255.0) + frame[200:320, 10:130, c] * (1.0 - emoji_face[:, :, 3] / 255.0) 113 114 cv2.imshow('face', frame) 115 if cv2.waitKey(10) & 0xFF == ord('q'): 116 break 117 # 釋放系統攝像頭,關閉窗口 118 video_captor.release() 119 cv2.destroyAllWindows()
到此整個Demo就實現完成了。
3.個人遇坑以及總結
(1)數據預處理過程,大多都需要用到pandas和numpy這兩個庫,所以必須能夠較為熟練的掌握這兩個庫的使用
(2)網上的考察資料中對於調用 cascade_classifier.detectMultiScale這個函數時,對其一個參數scaleFactor設置為1.3,這直接導致對於人臉的識別不夠敏感,上網參閱相關資料后,得知這個值一般設置為1.1較為合適,且對於人臉的識別也比較敏感
(3)因為對於opencv這個庫使用不夠熟練,一開始一直碰壁,所以你要是和我一樣,建議先去這個網站了解一下OPENCV的大致使用方法:https://docs.opencv.org/trunk/d6/d00/tutorial_py_root.html
4.該模型的不足以及日后的發展
(1)目前划分的微表情類別較少,難以覆蓋人類多種交織的表情。
(2)對於某人表情識別的准備率還不夠高,例如厭惡。
(3)模型未能處理多幀連續表情的變化,這將限制我們對少數微表情的識別的准確率。
5.完善算法以及平台的應用
(1)采用LSTM(長短時記憶算法),新增加一個時間維度用於記錄多幀微表情的變化,然后在結果DNN模型完善算法。
(2)可以將此項目應用於檢測疲勞駕駛以及刑偵等領域
參考資料:https://docs.opencv.org/trunk/d6/d00/tutorial_py_root.html
https://blog.csdn.net/labPqsdr/article/details/80704969
以上就是本次Demo的完成過程以及體會,歡迎交流