ディープラーニングでおそ松さんの六つ子は見分けられるのか? 〜準備編〜
最近おそ松さんというアニメが流行っていますね。 6つ子のおそ松くんのアニメを現代版にアレンジした作品なのですが、その過程でそれぞれの兄弟の特徴が付けられています。
左から、おそ松、から松、チョロ松、一松、十四松、とど松で、順に長男次男三男・・・となっています。

簡単にまとめると、このようになります。
| 生まれ | 名前 | 色 | 特徴 |
|---|---|---|---|
| 長男 | おそ松 | 赤 | クズ |
| 次男 | から松 | 青 | ナルシスト |
| 三男 | チョロ松 | 緑 | ツッコミ、意識高い系 |
| 四男 | 一松 | 紫 | コミュ障 |
| 五男 | 十四松 | 黄色 | マイペース |
| 六男 | とど松 | ピンク | 甘え上手、腹黒 |
それぞれの色を着ているときは、簡単に見分けられますが、そうでないときは見分けるのに困難を伴います。
髪や目つきにも特徴があるので、見分けることができるので、このような表を作ってらっしゃる方もいます。

それでも結構苦労したので、同じくディープラーニングで学習させたモデルで判別できないか、試してみました。 今回はその準備編です。
学習データの作成
まずディープラーニングでおそ松さんの顔を学習させるためには、ラベリングされた大量の顔画像が必要になります。 アニメ顔判別の先人がすでにいらっっしゃり、ラブライブの人は6000枚、ご注文はDeep Learningですか?の人は12000枚収集したとのことなので、数千枚オーダーの画像は必要だと判断しました。
画像収集の方針
先人たちは、アニメのスクリーンショットから顔の部分だけ抽出して、手動でラベリングされているので、同様の手法で行こうと考ええましたが、以前ももクロ画像のラベリング作業の経験から難しいんじゃないかと考えました。
以前ももクロのメンバーでこれをやろうとして、画像をスクレイピング・スクロールして収集、顔を切り抜いて順にラベリングしていったのですが、顔だけでラベリングするのは効率が良くありませんでした。普段ももクロは夏菜子ちゃんだったら赤、れにちゃんだったら紫というように、それぞれのメンバーの色をまとっていることが多く、想像以上に色の情報で個々を判断していること、また7年間分の写真が存在するため、成長過程で顔の形が変わったり、痩せたり太ったりで、なかなか効率が上がりませんでした。
おそ松さんの場合も同じようなことが考えられます。タダでさえ似ているのに、顔だけ切り抜いてラベリングするのは効率が悪いと判断しました。 そこで、アニメにおける前後の文脈や服装、位置などの情報とともにラベリングできるよう、以下の方法を考えました。
1, アニメを流しながら、全体のスクリーンショットを撮る
2, 指定ディレクトリに保存され、そこに入った写真に対して自動で顔を切り抜いてくれるようにする
3, 切り抜かれた顔写真をデスクトップでフォルダ分けしていく
切り抜きについて 〜OpenCV+AnimeFace編〜
始めは、ラブライブの人が使っていたOpenCV+AnimeFaceを元に、毎秒指定フォルダを監視、写真ファイルが入ったら顔を切り抜いて別フォルダに入れるように考えました。 以下書いたソース
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# crop face from all picture in specific directory in every second
#
# usage: ./crop_face_always.py origin_directory dist_directory
#
import cv2
import math
import numpy as np
import os
import sys
import glob
import time
import shutil
def cut_face(origin_path, dist_path) :
# 各画像の処理
img_path_list = glob.glob(origin_path + "/*")
for img_path in img_path_list:
print img_path
# ファイル名解析
base_name = os.path.basename(img_path)
name,ext = os.path.splitext(base_name)
if (ext != '.jpg') and (ext != '.jpeg') and (ext != '.png'):
print "not a picture"
continue
img_src = cv2.imread(img_path, 1)
# グレースケールに変換
img_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
#顔判定
faces = cascade.detectMultiScale(img_gray, scaleFactor=1.2, minNeighbors=1, minSize=(10, 10))
# 顔があった場合
if len(faces) > 0:
i = 0
for (x,y,w,h) in faces:
face = img_src[y:y+h, x:x+w]
file_name = dist_path + name + "_" + str(i) + ext
cv2.imwrite(file_name, face )
i += 1
else:
print "not find any faces"
shutil.move(img_path, origin_path + "/../finished/")
if __name__ == '__main__':
# 顔判定で使うxmlファイルを指定する。
cascade_path = os.path.dirname(os.path.abspath(__file__)) + "/lbpcascade_animeface.xml"
cascade = cv2.CascadeClassifier(cascade_path)
# 入力の読み込み
if len(sys.argv) < 2 :
exit()
else :
origin_path = sys.argv[1]
if len(sys.argv) < 3 :
dist_path = os.path.dirname(os.path.abspath(__file__)) + "/face/"
else :
dist_path = sys.argv[2]
# 毎秒画像を処理
while(1):
cut_face(origin_path, dist_path)
time.sleep(1)
しかしながら、このフォルダにおそ松さんの画像を放り込んでも、顔画像は一向に出て来ません。 試しにラブライブの画像を入れてみると、すぐに人数分出てきました。どうやら、おそ松さんのアニメ風の顔は、アニメ顔検出用のlbpcascade_animeface.xmlを作った時のデータに入っておらず、おそ松さんとは相性が悪いようです。自分でhaarcascadeのxmlを作成するには、それだけで数千枚の画像が必要になるので、とっても厳しいです。
顔の切り抜き 〜dlib使用編〜
そこでこちらに書かれていた方法を試します。
こちらはdlibという画像ライブラリに実装されている、HOG特徴量をSVMで処理して検出器を作成する手法です。こちらだと画像の枚数が少なくてすみ、またツールも既存のものを使えるため、こちらを試しました。
流れとしては、
1, アニメからスクショをたくさん撮る
2, 画像から顔の位置を抽出、座標と画像パスのデータを作成
3, trainとtestに分け、xml形式ファイルに変換
4, SVMで学習させ、検出器を作る
となります。
1, アニメからスクショをたくさん撮る
動画再生しながら、スクリーンショットを取ります。 スクショの名前及び保存場所の変更はこちらに手順が書いてあります。 MacbookProでCmd+Shift+3でスクショを撮ると、サイズが大きすぎるので、こちらを参考に1280*800に縮小します。
2, 画像から顔の位置を抽出
画像と顔位置のデータを作成します。 上記の顔切り取りのpythonスクリプトを、一部修正して使わせて頂きました。 とても便利な作りになっていて、助かりました。
画像のフォルダを指定して実行すると、画像が現れます。
ドラッグ&ドロップで領域を指定できるので、このように顔を囲います。

ある程度正方形に近くないと学習できないようで、比率が悪いと線が赤になり、確定できません。

ちゃんと緑の状態でドラッグを解除すると、白色で四角形が残ります。このように全ての顔を囲っていきます。もし間違えてしまっても、「d」キーを押せば取り消せます。

「n」キーを押すと次の画像に行き、全ての画像を終えるまで繰り返します。 今回は150枚を約1時間ほど処理しました。
こちらが一部修正したソースです。 修正点は、ファイルリストの入力を標準入力からディレクトリのみを標準入力にしたのと、後述する変換スクリプトのためのバグ修正です。
#!/usr/bin/env python
#! -*- coding: utf-8 -*-
import cv2
import numpy as np
import sys
import glob
drawing = False
sx, sy = 0, 0
gx, gy = 0, 0
rectangles = []
ok = False
def draw_circle(event,x,y,flags,param):
global sx, sy, gx, gy, drawing
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
sx,sy = x,y
elif event == cv2.EVENT_MOUSEMOVE:
if x > 0 and x < img.shape[1]:
gx = x
if y > 0 and y < img.shape[0]:
gy = y
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
w = abs(sx-x)
h = abs(sy-y)
if w < h * 1.1 and h < w * 1.1:
rectangles.append([(sx, sy), (x, y)])
img = np.zeros((512, 512, 3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle)
i = 0
origin_path = sys.argv[1]
img_path_list = glob.glob(origin_path + "*")
while i < len(img_path_list):
rectangles = []
while True:
img = cv2.imread(img_path_list[i])
for r in rectangles:
cv2.rectangle(img, r[0], r[1], (255,255,255), 2)
if drawing:
w = abs(sx-gx)
h = abs(sy-gy)
if w < h*1.1 and h < w*1.1:
color = (0, 255, 0)
else:
color = (0, 0, 255)
cv2.rectangle(img, (sx,sy), (gx,gy), color, 2)
cv2.imshow('image', img)
k = cv2.waitKey(1) & 0xFF
if k == ord('d'):
if rectangles:
rectangles.pop()
elif k == ord('n'):
print img_path_list[i],
print str(len(rectangles)),
for r in rectangles:
x = min(r[0][0], r[1][0])
y = min(r[0][1], r[1][1])
w = abs(r[0][0] - r[1][0])
h = abs(r[0][1] - r[1][1])
print x, y, w, h,
print
break
elif k == ord('b'):
print 'delete previous line!'
if i > 1:
i -= 2
break
elif k == ord('q'):
sys.exit()
i += 1
実行コマンドはこんな感じです。
$ ./labeling.py ~/Desktop/osomatsu_SS/ > osomatsu.txt
実行が終わると、このような形式で切り取った顔の座標情報が記録されます。
/hoge/Desktop/osomatsu_ss/SS100.png 5 616 131 167 176 401 237 164 156 195 99 136 148 124 283 125 133 729 112 115 122 /hoge/Desktop/osomatsu_ss/SS101.png 1 595 76 461 467 /hoge/Desktop/osomatsu_ss/SS102.png 4 594 43 98 90 71 204 124 122 153 347 114 125 196 25 80 76
画像パス 顔個数 x座標 y座標 横幅 縦幅 といった感じです。
3, trainとtestに分け、xml形式ファイルに変換
先ほど作成したデータを、検出器作成器で使えるようxml形式に変換します。 こちらも上記の変換スクリプトをそのまま使いました。
訓練用とテスト用に2種類必要なので、headコマンドで150枚分を120:30に分けて、xmlを生成します。
$ g++ -o gen_xml gen_xml.cpp $ head -120 osomatsu.txt > train.txt $ ./gen_xml > training.xml $ tail -30 osomatsu.txt > train.txt $ ./gen_xml > testing.xml
4, SVMで学習させ、検出器を作る
最後に検出器を作ります。dlibのインストールはこちらあたりを参照してください。 公式サンプルをもとに、検出器を作ったコードはこんな感じです。
#!/usr/bin/python
#! -*- coding: utf-8 -*-
import os
import sys
import glob
import dlib
from skimage import io
# 入力に画像ディレクトリパスを入れなかった時の警告
if len(sys.argv) != 2:
print(
"Give the path to the examples/faces directory as the argument to this "
"program. For example, if you are in the python_examples folder then "
"execute this program by running:\n"
" ./train_object_detector.py ../examples/faces")
exit()
faces_folder = sys.argv[1]
# 学習時のパラメータ
options.add_left_right_image_flips = True
options.C = 5
options.num_threads = 4
options.be_verbose = True
# 読みこむファイルパス
training_xml_path = os.path.join(faces_folder, "training.xml")
testing_xml_path = os.path.join(faces_folder, "testing.xml")
# 学習
dlib.train_simple_object_detector(training_xml_path, "detector.svm", options)
# 精度表示
print("") # Print blank line to create gap from previous output
print("Training accuracy: {}".format(
dlib.test_simple_object_detector(training_xml_path, "detector.svm")))
print("Testing accuracy: {}".format(
dlib.test_simple_object_detector(testing_xml_path, "detector.svm")))
こちらを実行するコマンドはこちらです。
$ cp training.xml ~/Desktop/osomatsu_SS/ $ cp testing.xml ~/Desktop/osomatsu_SS/ $ ./gen_detector.py ~/Desktop/osomatsu_SS/
これでdetector.svmという検出器が作成されます。こちらを使っておそ松さんがちゃんと検出されるか試します。
detectorを使うサンプルコードはこちらです。
#!/usr/bin/env python
#! -*- coding: utf-8 -*-
import os
import sys
import dlib
import numpy as np
import cv2
# 読みこむ画像
img = cv2.imread(sys.argv[1],1)
out = img.copy()
# 検出
detector = dlib.simple_object_detector("detector.svm")
dets = detector(img)
# 四角形描写
print str(len(dets))
for d in dets:
cv2.rectangle(out, (d.left(), d.top()), (d.right(), d.bottom()), (0, 0, 255), 2)
cv2.imshow('image',out)
cv2.waitKey(0)
cv2.destroyAllWindows()
検出結果
結果はこのようになっています。
OpenCV+AnimeFaceでは誰一人として検出されなかったおそ松さんたちが、ちゃんと全員検出されています。
重なっていても、ちゃんと検出できるようです。
しかしたまに検出されない顔もあります。
まだ精度向上の余地はあるが、そこそこ使える検出器が手軽に作れました。 パラメータを変更してみたり、訓練データを選別して精度を向上させることも考えましたが、今回はとりあえずこれで進めます。
画像収集
先ほど述べた以下の手順で画像を集めます。
1, アニメを流しながら、全体のスクリーンショットを撮る
2, 指定ディレクトリに保存され、そこに入った写真に対して自動で顔を切り抜いてくれるようにする
3, 切り抜かれた顔写真をデスクトップでフォルダ分けしていく
2を行ってくれるスクリプトは以下です。
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# crop face from all picture in specific directory in every second
#
# usage: ./crop_face_always.py origin_directory dist_directory
#
import cv2
import math
import numpy as np
import os
import sys
import glob
import time
import shutil
import dlib
def cut_face(origin_path, dist_path) :
# 各画像の処理
img_path_list = glob.glob(origin_path + "/*")
detector = dlib.simple_object_detector("detector.svm")
for img_path in img_path_list:
print img_path
# ファイル名解析
base_name = os.path.basename(img_path)
name,ext = os.path.splitext(base_name)
if (ext != '.jpg') and (ext != '.jpeg') and (ext != '.png'):
print "not a picture"
continue
img_src = cv2.imread(img_path, 1)
#顔判定
dets = detector(img_src)
# 顔があった場合
if len(dets) > 0:
i = 0
for d in dets:
face = img_src[d.top():d.bottom(), d.left():d.right()]
file_name = dist_path + name + "_" + str(i) + ext
cv2.imwrite(file_name, face )
i += 1
else:
print "not find any faces"
shutil.move(img_path, origin_path + "/../finished/")
if __name__ == '__main__':
# 入力の読み込み
if len(sys.argv) < 2 :
exit()
else :
origin_path = sys.argv[1]
if len(sys.argv) < 3 :
dist_path = os.path.dirname(os.path.abspath(__file__)) + "/face/"
else :
dist_path = sys.argv[2]
# 毎秒画像を処理
while(1):
cut_face(origin_path, dist_path)
time.sleep(1)
こんな感じでアニメを見ながら作業を進めます。

ペースは大体1時間で500枚程度。コツコツ作業して、2~5話から5644枚のおそ松さんたちのラベリングされた画像を集めました。1話は色々と違うのでいれませんでした。 以上で準備編は終わりです。次回はいよいよ、集めた画像を用いてディープラーニングで学習させます。