2019年版いろんなアイドルの平均顔を作ってみた(Python)
今更ながら、アイドルの平均顔を作ってみたくなったのでPythonで作ってみた。
先に結果を載せるとこんな感じになりました。(大量に作ったので、いい感じにできたやつを先に紹介します)
※各グループの公式サイトの画像を使用しているので、完成度にはばらつきがあります。
日向坂や他の48グループ、ハロプロ、韓国アイドルなどいろんなグループの平均顔もつくったのでこの記事の一番下にまとめて載せておきました。
坂道シリーズの期生ごとの平均顔や、48のチームごとの平均顔も作っています。
目次
画像を集める
まずは、平均顔を作りたいアイドルグループの画像を探します。
平均顔を作るためにはある程度正面を向いた画像がよろしいので、公式サイトの宣材写真的なやつをとってくることにしました。
画像は自分で一個一個保存していってもいいですが、AKBとかは人数が多いのでスクレイピングしてしまったほうが楽だと思います。
私は、こんな感じで画像を一気に集めました。保存するファイル名とかは適当です。
import requests from bs4 import BeautifulSoup URL = 'https://www.akb48.co.jp/about/members/' #AKB48の公式サイト images = [] soup = BeautifulSoup(requests.get(URL).content,'lxml') for link in soup.find_all("img"): if link.get("src").endswith(".jpg"): images.append(link.get("src")) for i, target in enumerate(images): try: resp = requests.get(target) except: continue with open('./akb_{}.jpg'.format(i), 'wb') as f: f.write(resp.content)
スクレイピングはこちらのサイトが参考になりました。
画像の左右を反転する
取得した画像は、右を向いている人や左を向いている人、正面を向いている人などバラバラです。これでは平均顔がきれいに作れないので、向きを合わせるために画像を反転させます。
今回は全員が向って左向きになるように合わせました。正面の人はそのままにします。
import cv2 img_path = "./image_path/filename.jpg" img = cv2.imread(img_path) #画像読み込み img = cv2.flip(img, 1) #反転 cv2.imwrite(img_path, img) #上書き保存
画像を回転させ、顔がまっすぐになるようにする
すべての画像の顔の部分がしっかり重なった、きれいな平均顔を作るためには集めてきた画像を回転させて顔をまっすぐにしてやる必要があります。
そのために、今回はdlibという機械学習ライブラリを使って顔器官(顔のパーツ)を検出し、左右の目の高さがそろうように画像を回転させようと思います。
dlibで顔を検出
まずは、dlibで顔検出を行います。
dlibのget_frontal_face_detector()により、顔の位置の矩形情報を得ることができます。
今回は、画像に写っている顔は1つだけという想定でやっていきます。
import cv2 import dlib img_path = "./saito_asuka.jpg" img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) detector = dlib.get_frontal_face_detector() faces = detector(img) for i, rect in enumerate(faces): cv2.rectangle(img, (rect.left(), rect.top()), (rect.right(), rect.bottom()), (255, 255, 0), thickness=2) #得られた顔の位置に矩形を描画
矩形を描画した画像を表示してみるとこんな感じになっています。
このようにして、画像の顔の位置を得ることができました。
dlibで顔のパーツを検出
次に、dlibのshape_predictor()で顔器官の検出をします。
shape_predictor()は人の顔の画像を入力すると、目や口や鼻などの顔の重要なランドマークの位置を特定してくれます。
顔器官の検出を行うには、学習済みデータが必要です。
http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
こちらを解凍して使いました。
from imutils import face_utils predictor_path = "./shape_predictor_68_face_landmarks.dat" predictor = dlib.shape_predictor(predictor_path) faces = detector(img) for i, rect in enumerate(faces): shape = predictor(img, rect) shape = face_utils.shape_to_np(shape) #顔のランドマークの位置に対応した68個の座標を得る for point in shape: cv2.circle(img, tuple(point), 1, (255, 255, 0), -1) #得られたランドマークの座標を画像に描画
そうすると、こんな感じになります。
点の数は68個あり、それぞれの点の番号と顔のパーツが対応している。
例えば、向かって左目は36~41番目の点と対応しており、右目は42~47番目と対応している。
左右の目の位置を求める
次に、左右の目の位置を求めていきます。
先ほど得た68個を使います。
left_eye_center = np.zeros(2) right_eye_center = np.zeros(2) for left_eye in shape[36:42]: #向かって左目 left_eye_center += left_eye for right_eye in shape[42:48]: #右目 right_eye_center += right_eye left_eye_center = left_eye_center / 6 right_eye_center = right_eye_center / 6 #6点の座標の平均をとり、それぞれの目の中心とする cv2.circle(img, tuple(left_eye_center.astype(np.int)), 2, (255, 255, 0), -1) cv2.circle(img, tuple(right_eye_center.astype(np.int)), 2, (255, 255, 0), -1) #左右それぞれの目の中心に点を打つ
こうして得られた画像はこんな感じです。
左右それぞれの目のほぼ中心を求めることができています。
画像を回転させ、左右の目の高さを合わせる
左右の目の座標がわかったので、顔がどれだけ傾いているかを計算することができます。
上図のように向かって左目の中心座標を原点とするXY座標系を考え、右目の中心点をP(x, y)とすると顔の傾きは
で得られます。
そして、左目を中心にして画像を 回転させてやれば顔の傾きはゼロになります。
import math row, col, ch = img.shape tan = (left_eye_center[1] - right_eye_center[1]) / (right_eye_center[0] - left_eye_center[0]) deg = math.degrees(math.atan(tan)) M = cv2.getRotationMatrix2D(tuple(left_eye_center.astype(np.int)), -deg, 1) #2次元回転を表すアフィン変換を求める img = cv2.warpAffine(img, M, (col, row), borderValue=(255, 255, 255)) #borderValueは領域外の色を指定
参考
画像の幾何学変換 — opencv 2.2 documentation
OpenCV - 画像座標系における回転、拡大縮小について - Pynote
こうしてできた画像がこちら
画像を回転させたことにより、しっかりと顔の傾きをなくすことができています。
画像を重ねるときの基準点を計算する
画像と画像を重ねて平均顔を作るときには、1つ目の画像の顔の部分と2つ目の画像の顔の部分がしっかりと重なるようにしなければなりません。
今回は、画像と画像の目の位置を合わせて重ねていくことにします。
先ほど求めた左目の中心点と右目の中心点の線分の中心を顔の基準点とし、そこが重なるようにすることで目の位置を合わせます。
下の図の赤い点が基準点です。
eye_center = (left_eye_center + right_eye_center) / 2 #eye_centerが基準点 #この基準点の座標は回転させる前の座標
いま、顔の傾きをなくすために画像を回転させているので、回転後の基準点の座標を計算してやる必要があります。
上図のように、点(x, y)を角度 回転させた点(X, Y)は加法定理より
となります。これを用いて、回転後の基準点を計算します。
rad = math.radians(-deg) #これが α (弧度法) x_eye_center_rotated = left_eye_center[0] + (eye_center[0] - left_eye_center[0]) * math.cos(rad) - (left_eye_center[1] - eye_center[1]) * math.sin(rad) #回転後の基準点のx座標 y_eye_center_rotated = left_eye_center[1] - (eye_center[0] - left_eye_center[0]) * math.sin(rad) - (left_eye_center[1] - eye_center[1]) * math.cos(rad) #回転後の基準点のy座標 cv2.circle(img, (int(x_eye_center_rotated), int(y_eye_center_rotated)), 2, (255, 0, 0), -1)
こうしてできた画像がこちら。
きちんと回転後の基準点の計算ができていますね。
複数の画像を重ね合わせ平均顔をつくる
いよいよ、画像を重ねて平均顔を作っていきます。
最終的なコードはこのようになりました。
import numpy as np import cv2 import dlib from imutils import face_utils import math import glob from copy import copy folder_path = "./nogizaka46/" #乃木坂の画像が詰まってるフォルダ img_path_list = glob.glob(folder_path + "*.jpg") detector = dlib.get_frontal_face_detector() predictor_path = "./shape_predictor_68_face_landmarks.dat" predictor = dlib.shape_predictor(predictor_path) for img_num, img_path in enumerate(img_path_list): img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) row, col, ch = img.shape faces = detector(img) for face_num, rect in enumerate(faces): shape = predictor(img, rect) shape = face_utils.shape_to_np(shape) left_eye_center = np.zeros(2) right_eye_center = np.zeros(2) for left_eye in shape[36:42]: left_eye_center += left_eye for right_eye in shape[42:48]: right_eye_center += right_eye left_eye_center = left_eye_center / 6 right_eye_center = right_eye_center / 6 #顔の大きさをそろえるときに使う #left_right_eye_dist = math.sqrt((left_eye_center[0] - right_eye_center[0]) ** 2 + (left_eye_center[1] - right_eye_center[1]) ** 2) tan = (left_eye_center[1] - right_eye_center[1]) / (right_eye_center[0] - left_eye_center[0]) deg = math.degrees(math.atan(tan)) rad = math.radians(-deg) eye_center = (left_eye_center + right_eye_center) / 2 M = cv2.getRotationMatrix2D(tuple(left_eye_center.astype(np.int)), -deg, 1) img = cv2.warpAffine(img, M, (col, row), borderValue=(255, 255, 255)) #回転中心の座標はint型になっているはずなので計算精度を上げる処理 left_eye_center = left_eye_center.astype(np.int) x_eye_center_rotated = left_eye_center[0] + (eye_center[0] - left_eye_center[0]) * math.cos(rad) - (left_eye_center[1] - eye_center[1]) * math.sin(rad) y_eye_center_rotated = left_eye_center[1] - (eye_center[0] - left_eye_center[0]) * math.sin(rad) - (left_eye_center[1] - eye_center[1]) * math.cos(rad) #最初の画像を基準として、その上に次々重ねていくイメージ if img_num == 0: row_original = row col_original = col x_eye_center_rotated_original = x_eye_center_rotated y_eye_center_rotated_original = y_eye_center_rotated img_original = copy(img) #顔の大きさをそろえるときに使う #left_right_eye_dist_original = left_right_eye_dist #2つ目以降の画像の処理 else: #顔の大きさをそろえるときに使う #scale = left_right_eye_dist_original / left_right_eye_dist #originalとの左右の目の距離の比 #img = cv2.resize(img, (int(col * scale), int(row * scale))) #originalに合わせて画像をresize #x_eye_center_rotated = x_eye_center_rotated * scale #y_eye_center_rotated = y_eye_center_rotated * scale #imgのresizeに合わせて目の中心座標も変更 #1枚目の画像の基準点と2つ目以降の画像の基準点との距離 x_eye_center_dist_from_original = x_eye_center_rotated_original - x_eye_center_rotated y_eye_center_dist_from_original = y_eye_center_rotated_original - y_eye_center_rotated #基準点の距離分平行移動させて基準点を合わせる M_shift = np.float32([[1, 0, x_eye_center_dist_from_original], [0, 1, y_eye_center_dist_from_original]]) img = cv2.warpAffine(img, M_shift, (col_original, row_original), borderValue=(255, 255, 255)) #すべての画像のweightが等しくなるように重ねて平均顔完成 img_original = cv2.addWeighted(img_original, 1 - 1 / (img_num + 1), img, 1 / (img_num + 1), 0) cv2.imwrite("./average_face/nogizaka46.jpg", cv2.cvtColor(img_original, cv2.COLOR_RGB2BGR))
所々出てくる”#顔の大きさをそろえるときに使う”という部分は、顔の大きさが違う画像が含まれるときなどに使用します。
1枚目の基準となる画像の目と目の距離と、2枚目以降の目と目の距離の比から画像を拡大縮小して顔の大きさをそろえます。
今回は公式サイトの画像を使用しているので、同グループの顔の大きさや画像の大きさがほぼ一定なので使用しませんでした。
ただし、別グループどうしの平均顔を作る際には使うといいと思います。
そんなこんなで完成した画像がこちらです。
乃木坂46の平均顔はこんな感じらしいです。しっかりと目の位置があっているのでやりたかったことはできました。
最後に
ここまで読んでいただきありがとうございました。
ここから先は、今回作成したいろんなアイドルの平均顔を紹介していきます。
私の趣味にだいぶ偏った内容になっています。
みたいグループがなかったら、上のコードで作ってみてください。