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 の方にもチャレンジしてみたいと思います。
ではまた(^^)

2017-03-13

テキストだけHTMLから抽出する方法を考えてみた【ソースコード・ダウンロード可】

現在の開発でウェブ上のHTMLページから重要なテキストだけを取得する必要がでてきたのでどのようなアプローチがいいのかを考えてみました。

いろいろなアプローチを試しては失敗をしたのですが、最終的にある程度の精度を出せる方法に行きついたのでここで紹介したいと思います。


【抽出アルゴリズム】

実際にはアルゴリズムというほど複雑な方法ではありませんが、結局は「人間の目で見てテキストが密集している部分をグループ化する」というアプローチが功を奏しました。

流れとしては以下になります。
  1. もしテキスト間のHTMLタグが5つ以下なら(つまり近くにあるなら)そのテキストは全てひとつのテキストとして結合させる。
  2. 結合したテキストをひとつひとつチェックし、テキストの長さが100以上あれば、それはコンテンツとして残す。
文章ではわかりにくいと思いますので、HTML タグの例を見てみましょう。

(HTML例)

<div>
    <div>テキスト1</div>
    <div>テキスト2</div>
    <div>テキスト3</div>
</div>

<img src="***">
<img src="***">
<img src="***">

<div>
     <div>テキスト4</div>
</div>


テキストが近いければ結合する

まず、「テキスト1」と「テキスト2」 の間には HTML が2つ(</div>と<div>)だけです。
つまり HTML 構造的にいうと距離は「2ステップ」の位置にあるため近いテキストということになります。
なので、この2つのテキストは結合します。

では、「テキスト3」と「テキスト4」はどうでしょう?
間にあるのは、
</div></div><img><img><img><div><div>
なので7ステップです。

デフォルトの基準は5ステップ以下なら結合することになっているのでこれは「遠いテキスト」ということで結合はしません。

これを全てのテキストで実行すると、ほぼ「見た目で近いテキストが集まったグループ」が作成できることになります。


テキストが長ければコンテンツとして抽出する

グループ別のテキストが作成できたので次にこのテキストがある程度以上長ければ残し、短ければ重要度が低いと判断し削除します。

このフィルターを通過したテキストがコンテンツ・テキストということになります。


ただし

この方法でもパーフェクトではありません。
また、紹介した方法をする前に <br> タグなどを一時的に退避させるなど HTML の加工が必要になりますのでご注意ください。


ソースコードのダウンロード

ということで、このコンテンツ抽出アルゴリズム(細々とした HTML の加工を含んでいます)を PHP クラス「Shellless」として公開しました。
Github でダウンロードか composer でインストールできるかと思いますのでもし興味がありましたらぜひアクセスしてみてください。

https://github.com/SUKOHI/Shellless


今回は以上です。(^o^)