人の顔を見て鈴木絢音さんかどうかだけしか判断しないガチ恋人工知能を作りたい①

見切り発車のウケ狙いタイトル。ただの畳み込みニューラルネットワーク(CNN)を想定。人工知能とかいうワードどうにかして。それはさておき。

 

 

1. 事の経緯

起: 推しメンである鈴木絢音さんが21枚目シングルで選抜に入る。

 
承: これまで撮る機会もなかなか無かった選抜メンバーとのツーショットも増えるであろう。


転: あれ?あんまり他のメンバーのブログとか読まなくない?


結: 今後乃木坂46公式ブログでメンバーによって新たなブログが投稿された時、鈴木絢音さんの画像が載ってるブログだけ自動でピックアップしてくれ。

 

他のメンバーのブログも読めよ、で済む話だが、なかなか時間が無い。

2. 課題分割

  1. 機械学習関連: CNNの設計
    1. トレーニング画像どうしよう?
    2. 画像から顔だけ切り出したい。
    3. CNNの学習できるようなスペックのPC持ってない。
    4. CNNの設計
  2. web関連: ブログ更新を検出し、ページから画像を取り出してCNNへ入力、結果から起承転結の結を得る。

 

webとかには疎いので、ブログ更新を検出ってのがちょっときつい。一定時間ごとにページをクローリング、差分が出たら〜とかすればいいのかな。よく分かんないので、さしあたっては、特定のwebページを入力したら鈴木絢音さんがいるかどうかを出力するようなものを作ることを目標に。

  1. の課題についてはそれぞれ
    1. これまで死ぬほどiphoneのカメラロールに保存してきた画像を使おう。あるいは、鈴木絢音さん以外、のトレーニングデータはなんかまとまって置いてありそう。
    2. OpenCVでチャチャっと切り出せるのでは?先人に感謝
    3. なんか最近話題のGoogle Colaboratory使ってみよう。
    4. PythonとTensorflowで。まぁ層の構造はMNISTのやつと同じで出力を0(鈴木絢音さん)と1(鈴木絢音さん以外)にしてとりあえずは。

という感じで

こうして考えてみると、何億番煎じだよって感じがしてくる。だけどまぁ機械学習とか専門じゃないので、参考文献が多くてありがたいってことにしておく。

 

3. Google Colaboratory

Googleが提供している無料クラウド上でJupyter Notebookを実行できるやつ、無料なのにGPUで動かせるとかいうすごいやつ。研究室のPCにTensorflowとかの環境構築を行ったけど、あれはなかなかダルいものがあるので、環境構築の必要がないというのはとても良い。Googleの提供なので、当然Tensorflowは元から入ってるし、OpenCVもありがたいことに入ってるっぽい。ちょっとファイルの読み込み書き出しが手間っぽい。

 

4. 顔の認識、切り出し

テストとして、実際画像から顔認識ができるのか3枚の画像で試してみる

f:id:taroulion627:20180706040806j:plain
ayane1.jpg : 理想的な正面からの顔画像。

f:id:taroulion627:20180706040858j:plain
ayane2.jpg : 少しxy平面で斜め(今見てるモニターをxy平面として)ってる正面からの顔画像。

f:id:taroulion627:20180706040941j:plain
ayane3.jpg : 少しz方向で斜めってる顔画像。横顔とまではいかないけれども。自撮りはこの角度が多いので、これは認識してほしいところ


Google Colaboratory(以下Colab)では、ファイルの入出力がちょっと独特だけど、代わりにGoogle Driveからデータを持って来れる。PyDriveっつーモジュールを使うらしい

!pip install -U -q PyDrive

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

実行すると、Google Cloud SDKの認証リンクが出てくるので、アクセスしてVerification Codeを入力。



Google Drive上に入れた3つの画像、全部共有リンクを取得すると、
https://drive.google.com/open?id=hogehogeと表示されるので

id = 'hogehoge'
downloaded = drive.CreateFile({'id': id})
downloaded.GetContentFile('ayane1.jpg')

Google Drive上のファイルがダウンロードされる。

!ls

で確認すると実際ディレクトリに何があるか見れる。

OpenCVは使えるんだけど、カスケード分類器のファイルとかはディレクトリから参照のしようもないので、OpenCV公式GitHubで配布されているダウンロードしてColabのディレクトリに入れなきゃならないっぽい。haarcascadesフォルダをフォルダ毎ダウンロードするには、先ほどのファイルで使ったidの方法は使えないようなので、haarcascades.zipに圧縮して

id = 'hogehoge'
downloaded = drive.CreateFile({'id': id})
downloaded.GetContentFile('haarcascades.zip')
!unzip haarcascades.zip

とすれば良い。


準備は整ったので、切り出してみる。

import cv2

# モデルファイル(どれかを選ぶ)
#cascade_path = "haarcascades/haarcascade_frontalface_default.xml"
cascade_path = "haarcascades/haarcascade_frontalface_alt.xml"
#cascade_path = "haarcascades/haarcascade_frontalface_alt2.xml"
#cascade_path = "haarcascades/haarcascade_frontalface_alt_tree.xml"
#cascade_path = "haarcascades/haarcascade_profileface.xml"
#cascade_path = "haarcascades/haarcascade_mcs_nose.xml"

# 画像の読み込み
image = cv2.imread("ayane1.jpg")

# グレースケールに変換
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 評価器を読み込み
cascade = cv2.CascadeClassifier(cascade_path)

# 顔検出
facerect = cascade.detectMultiScale(
    gray,
    scaleFactor=1.11,
    minNeighbors=2,
    minSize=(30, 30)
)

if len(facerect) > 0:
    BORDER_COLOR = (255, 255, 255) 
    for rect in facerect:
        cv2.rectangle(
            image,
            tuple(rect[0:2]),
            tuple(rect[0:2] + rect[2:4]),
            BORDER_COLOR,
            thickness=2
        )

# 結果の画像を保存
cv2.imwrite("detected1_frontalface_alt.jpg", image)

upload_file_2 = drive.CreateFile()
upload_file_2.SetContentFile("detected1_frontalface_alt.jpg")
upload_file_2.Upload()

下の3行がGoogle Driveに画像を保存する文言。

結果f:id:taroulion627:20180706050654j:plainf:id:taroulion627:20180706050705j:plainf:id:taroulion627:20180706050714j:plain

うーん、OpenCV使うだけでチャチャっといけるかと思いきや、なかなかに斜めに対しての精度が低い。
ちなみに、上手くいけばf:id:taroulion627:20180706052758j:plain
1つの画像から複数人をちゃんと切り出してくれる。


識別に使っている特徴量が角度に対して不変ではない、ということなので、この方法を使って斜めを検出するには、画像の方を斜めにして顔を正面にするしかないようで...

import cv2
import numpy as np
import os
from math import ceil

cascade_path = "haarcascades/haarcascade_frontalface_alt.xml"

image = cv2.imread("ayane2.JPG")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cascade = cv2.CascadeClassifier(cascade_path)

org_width = image.shape[1]
org_height = image.shape[0]
i = 0

for j in range(0,71):
            # 拡大画像の作成
            big_img = np.zeros((org_height * 2, org_width * 2 ,3), np.uint8)
            big_img[int(ceil(org_height/2.0)):int(ceil(org_height/2.0*3.0)), int(ceil(org_width/2.0)):int(ceil(org_width/2.0*3.0))] = image

            # 画像の中心位置
            center = tuple(np.array([big_img.shape[1] * 0.5, big_img.shape[0] * 0.5]))

            # 画像サイズの取得(横, 縦)
            size = tuple(np.array([big_img.shape[1], big_img.shape[0]]))

            # 回転させたい角度
            angle = 5.0 * float(j)
            # 拡大比率
            scale = 1.0

            # 回転変換行列の算出
            rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)

            # アフィン変換
            img_rot = cv2.warpAffine(big_img, rotation_matrix, size, flags=cv2.INTER_CUBIC)
            rot_gray = cv2.cvtColor(img_rot, cv2.COLOR_BGR2GRAY)

            #顔判定
            faces  =  cascade.detectMultiScale(img_rot, scaleFactor=1.11, minNeighbors=2, minSize=(30, 30))
            
            if len(faces) >0:
              BORDER_COLOR = (255, 255, 255)
              for rect in faces:
                cv2.rectangle(
                    img_rot,
                    tuple(rect[0:2]),
                    tuple(rect[0:2] + rect[2:4]),
                    BORDER_COLOR,
                    thickness=2
                )
                file_name = "detected_ayane2_frontalface_alt_face_" + str(i) +".jpg"
                cv2.imwrite(file_name, img_rot)
                
                upload_file_2 = drive.CreateFile()
                upload_file_2.SetContentFile(file_name)
                upload_file_2.Upload()
            
                i += 1
            
            else :
              print('does not have any faces')

と5度ずつ回転させる方法をとる。

ayane2.jpgでの結果の数例を挙げる
f:id:taroulion627:20180706063604j:plainf:id:taroulion627:20180706063607j:plainf:id:taroulion627:20180706063611j:plainf:id:taroulion627:20180706063615j:plainf:id:taroulion627:20180706063617j:plain


確かに顔が検出できたものもあったが、なんせfor文で5度ずつ回転させるなんて作業なので凄く時間がかかるし、誤検出のせいで出力されるファイルの数も多すぎる。

ayane3.jpgの方でも顔を検出できているものもあった。
f:id:taroulion627:20180706064459j:plainf:id:taroulion627:20180706064503j:plainf:id:taroulion627:20180706064507j:plain

なんにせよ何百枚とトレーニングデータを必要とするのに、データを成形するのに1枚1枚こんなかかっていたんじゃ一生かかっても終わらん。

カスケード分類器には、顔だけじゃなく、目を識別するのもある。これと組み合わせて、誤検出を減らしていくという方法を考える。




とりあえずここで一旦終わる。



CNNに全然辿り着かなかった上に、調べれば簡単に出てくる情報ばかりの新規性もへったくれもあったもんじゃないブログでした。


まぁでもGoogle Colaboratoryは便利ということがわかったのでノートパソコンで気軽にぢーぷらーにんぐができそうです。


単純な興味として、顔認識とかの分類問題って基本的には同等の集合で分類するけど、"1人"と"それ以外"の集合を分類するのって、それよりもよりaccuracyの高い結果になるのかな?っていうのがあります。

ももクロの分類とかは軽く調べてみても結構な人数の人がやっていて、それは分類が5人っていうちょうど良い感じだからってのもありますよね。

乃木坂46の場合は全メンバーを分類するとしたら結構辛いことになるし、実際それをやろうとしている人もいましたが、実用に耐えないaccuracyしか出せないという結果に落ち着いているような気がします。

乃木坂46のメンバーの画像を入力して、「これは◯◯さんですね」と出力させるような乃木坂46アイドル博士AIを作るのは困難ですが、そうではなくてただ「推しメンだ!」「推しメンじゃない」のガチ恋AIであればなんか上手く行きそうだし、オタクじゃない誰かがこの子誰だろう?と気になった時に役立つなんだか凄いもの、よりは、普段からオタクの俺にとって作業効率をあげるための1つの手段、として使いたいものです。