2017-03-23

機械学習で顔の向きを取得する【scikit-learn】

今回、以前投稿した「顔の入れ替えを試してみた」で取得した顔データからその顔がどっちを向いているのか(角度)?を取得する必要がでてきたので機械学習の scikit-learn を使って実現してみました。
元々は人力で公式を作ろうとしてみましたが、数学の基礎知識が乏しい私には難かったため、これを機に以前からずっと気になっていた機械学習を導入することにしました。

そして、結果としては満足できる精度になりそうになったのでここにその作業の手順を残しておこうと思います。

追記: joblib は使わず pickle を使うようコードを変更しました。 (2017.03.24)




【おおまかな手順】
  • 角度データのある顔画像を用意
  • 顔データを加工して機械学習&結果を保存
  • 学習結果を使って未知の画像から顔の角度を取得

【実際の手順】 

まず角度データつきの顔画像を探しました。
機械学習で一番骨が折れるのはデータ収集です。
ある程度の数がないと精度を高めることができないためです。

ただ、今回はテストということで数は少ないですが以下のページからダウンロードして試してみました。

http://www-prima.inrialpes.fr/perso/Gourier/Faces/HPDatabase.html


次にこの画像から学習データとラベルを取得して学習させます。
(今回は顔が左右どちらにどのくらいの角度向いているかを学習させます。)


学習データ

データは、以下のような顔の右側と左側の比率を使うことにしました。

(左のこめかみから鼻までの距離 + 左のほほから鼻までの距離) / (右のこめかみから鼻までの距離 + 右のほほから鼻までの距離)

※2本ずつの距離を使ったのは縦方向の回転(pitch)の影響が少なくなるのではないかと思ったからです。


ラベル

ラベル(学習後、テストデータを入れると取得できる答え)はダウンロードしたファイル名に顔の角度が「+30」や「-45」などの形で含まれているのでこれを正規表現で切り出してそのまま利用します。

※つまり、ある顔データを入力すると「この顔の角度は+15(左に15度)です」などという形で答えを教えてくれるという形になります。


【実際のコード】

実際の機械学習コードは以下になります。

※このコードを使うには 「顔の入れ替えを試してみた」 で紹介した dlib の顔検出環境が必要になりますので事前に準備しておく必要があります。
(後で知りましたが、dlib は python のパッケージマネージャーの pip や anaconda で楽に準備できるようですね^^;)

※ path/to/***** となっている部分は自分の環境に合わせてください。テストした環境は python 2.7.12 です。

 # -*- coding: utf-8 -*-  
 import sys  
 import cv2  
 import dlib  
 import glob  
 import re  
 import pickle
 import numpy as np  
 from sklearn import svm
 def get_distance(landmarks, ids):  
   id1 = ids[0]  
   id2 = ids[1]  
   point1 = landmarks[id1]  
   point2 = landmarks[id2]  
   return np.linalg.norm(np.array(point2)-np.array(point1))  
 def get_face_h_ratio(landmarks):  
   length1 = get_distance(landmarks, [0,30])  
   length2 = get_distance(landmarks, [16,30])  
   length3 = get_distance(landmarks, [4,30])  
   length4 = get_distance(landmarks, [12,30])  
   return (length1+length3)/(length2+length4)  
 predictor = dlib.shape_predictor('/path/to/shape_predictor_68_face_landmarks.dat') # Predictorファイル  
 detector = dlib.get_frontal_face_detector()  
 train_angles = []  
 train_labels = []  
 for i in range(1,16):  
   dir_path = '/path/to/head_pose_images/Person%02d/*' % i  # ダウンロードした顔画像フォルダ  
   print dir_path  
   paths = glob.glob(dir_path)  
   for path in paths:  
     m = re.search(r'([\+\-]+([0-9]{1,2}))([\+\-]+([0-9]{1,2}))\.jpg$', path)  
     if m:  
       im = cv2.imread(path, cv2.IMREAD_COLOR)  
       rects = detector(im, 1)  
       if len(rects) == 0:  
         continue  
       landmarks = np.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])  # 顔ポイントを取得  
       if landmarks is not None:  
         label = m.group(1)           # ファイル名から顔の左右角度を切り出す(yaw)  
         ratio = get_face_h_ratio(landmarks)   # 顔の左右距離から比率を取得  
         train_angles.append([ratio])  
         train_labels.append(label)  
 clf = svm.SVC()  
 clf.fit(train_angles, train_labels) # 取得したデータで機械学習する  
 pickle.dump(clf, open('yaw_angles.pkl', 'wb')) # 学習した結果を後でも使えるようにファイル保存  

このコードを実行すると「yaw_angle.pkl」というファイルが作成されます。
これが学習結果になりますので後で顔角度を取得したい場合は、以下のようにこのファイルをロードして使うことになります。

 path = 'checking_image.jpg'  
 im = cv2.imread(path, cv2.IMREAD_COLOR)  
 rects = detector(im, 1)  
 if len(rects) == 0:  
   sys.exit()  
 landmarks = np.matrix([[p.x, p.y] for p in predictor(im, rects[0]).parts()])  # 顔ポイントを取得  
 if landmarks is not None:  
   with open('yaw_angles.pkl', 'rb') as f:
      clf = pickle.load(f)
   ratio = get_face_h_ratio(landmarks)   # 顔の左右距離から比率を取得  
   pre = clf.predict([ratio])  
   print pre  # 配列で答え(ここでは角度)が返ってきます。  

※ 注: ここでは import は省略しています。

以上が今回の機械学習の手順になります。
ただし、途中でも書きましたがこれだけでは学習データが少ないと思いますので他のデータも必要になってくるかと思います。

それにしても機械学習はすごいですね。
今回は左右の顔の向き(yaw)でしたが、同じようにすることで上下(pitch)も学習できるでしょうし、顔だけでなくインターネット上に無限にあるデータをうまく学習させればいろいろと面白いことができるのではないでしょうか。

今後は tensorflow の方にもチャレンジしてみたいと思います。
ではまた(^^)

3 件のコメント :

  1. SyntaxError: Missing parentheses in call to 'print'. Did you mean print(dir_path)?

    返信削除
  2. 初めまして
    自分も写真から顔認証し、顔の向き、角度を数字として出す必要が出てきたのでこちらのサイトを参考にしました。

    しかし、python3系統で行なっており、27行目の
    print dir_path
    がエラーコードとして
    SyntaxError: Missing parentheses in call to 'print'. Did you mean print(dir_path)?
    上記のようになってしまいます

    初心者で拙い質問かも知れませんが、3系統で動作させるにはどうすれば良いかご教授頂ければ幸いです

    返信削除
  3. Appreciate it! An abundance of posts. Thanks for ones marvelous posting! I really enjoyed reading it, you might be a great author.
    I will be sure to bookmark your blog and will eventually come back later on.

    Try to check my webpage - 부산오피
    (jk)

    返信削除