Edited at

ホームビデオの動画から顔を検出して画像を保存したら顔ではないものが多く誤検出されていたので、転移学習とクラスタリングを使って半自動で顔とそれ以外のものを判別する作業をまとめてみた


はじめに

タイトルが長くて申しわけありません...(汗)

内容はタイトルに書いてある通りです。


環境


  • MacPro

    (Quad-Core Intel Xeon, RAM 64GB, SSD 512GB + HDD 4TB)

  • Python3.6 (Anaconda)

  • OpenCV

  • TensorFlow (CPU版)

  • Keras


顔画像の抽出

以下のスクリプトを作成し、「haarcascade_frontalface_alt.xml」と「sample.mp4」というファイルを同じフォルダに入れ、「faces」というフォルダを作成しておく。


face_extraction.py

import cv2

if __name__ == '__main__':

# 定数定義
ESC_KEY = 27 # Escキー
INTERVAL= 33 # 待ち時間
FRAME_RATE = 30 # fps

# ウィンドウ名
WINDOW_NAME = "extraction"

# フォント
font = cv2.FONT_HERSHEY_DUPLEX
font_size = 1.0

# 描画
color = (0, 0, 225)
pen_w = 2

# 顔検出
cascade_file = "haarcascade_frontalface_alt.xml"
minSize = (100, 100)

# 動画ファイル
mov = "sample.mp4"

# プレフィックス
prfx = "smpl"

# 動画取得
cap = cv2.VideoCapture(mov)

# 読込開始
end_flag, c_frame = cap.read()

# ウィンドウの準備
cv2.namedWindow(WINDOW_NAME)

# 分類器の生成
cascade = cv2.CascadeClassifier(cascade_file)

# カウンター
cnt_frame = 0
cnt_face = 0

text = ""

# 変換処理ループ
while end_flag == True:

img = c_frame
img_out = img.copy()

# 白黒画像に変換
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 画像内の顔を検出
face_list = cascade.detectMultiScale(img_gray, minNeighbors=3, minSize=minSize)

for (pos_x, pos_y, w, h) in face_list:

# 検出した顔を切り出す
img_face = img[pos_y:pos_y+h, pos_x:pos_x+w]
img_face = cv2.resize(img_face, minSize)

# 検出した顔に四角を描画
cv2.rectangle(img_out, (pos_x, pos_y), (pos_x+w, pos_y+h), color, thickness = pen_w)

# 検出状況
text = "{}_{:09}_{:05}.jpg".format(prfx, cnt_frame, cnt_face)

# 検出した画像を保存
cv2.imwrite("faces/" + text, img_face)

cnt_face += 1

# 進捗表示
cv2.putText(img_out, text, (50, 50), font, font_size, color)

# フレーム表示
cv2.imshow(WINDOW_NAME, img_out)

# Escキーで終了
key = cv2.waitKey(INTERVAL)
if key == ESC_KEY:
break

# 次のフレーム読み込み
end_flag, c_frame = cap.read()
cnt_frame += 1

# 終了処理
cv2.destroyAllWindows()
cap.release()


以下のコマンドで実行。

python face_extraction.py

動画を再生するウィンドウが開き、顔を検出しながら顔画像がfacesフォルダに保存されていきます。


検出画像のクラスタリング

以下のスクリプトをfacesフォルダに作成。


clustering.py

# ライブラリ読込

from glob import glob
import pandas as pd
import numpy as np
import cv2
import random
import os
import datetime as dt

# 転移学習用ライブラリ
from keras.preprocessing import image
import numpy as np
from keras.applications.vgg16 import VGG16, preprocess_input, decode_predictions

# クラスタリング用ライブラリ
from sklearn.cluster import KMeans

# コンソール表示用関数
def printlog(msg):
print("[{:%H:%M:%S}] {}".format(dt.datetime.now(), msg))

# ファイル読込
printlog("loading files.")

files = glob("*.jpg")
printlog("{} files.".format(len(files)))

ref = list(range(len(files)))
random.shuffle(ref)

X = []
f = []

for n in ref:
img = image.load_img(files[n], target_size=(224, 224))
img = image.img_to_array(img)
X.append(img)
f.append(files[n])

X = np.array(X)
printlog("data shape {}".format(X.shape))

# 転移学習
printlog("transfer learning.")

model_vgg16 = VGG16(include_top=True, weights='imagenet', input_tensor=None, input_shape=None)
X = model_vgg16.predict(preprocess_input(X))

# クラスタリング
printlog("clustering.")

n_clst = 10
model = KMeans(n_clusters=n_clst)
pred = model.fit_predict(X)

df = pd.DataFrame({"y": pred, "file": f})
df.y.value_counts()

# 顔画像の仕分
printlog("changing file names.")

for n in range(n_clst):
for index, row in df[df.y == n].iterrows():
os.rename(row.file,"{}_{}".format(n, row.file))


以下のコマンドで実行。

$ python clustering.py

以下のようなメッセージが出力されファイル名にクラス番号が付与されます。

Using TensorFlow backend.

[18:43:29] loading files.
[18:43:29] 1000 files.
[18:43:29] data shape (1000, 224, 224, 3)
[18:43:29] transfer learning.
2019-06-08 18:43:32.066323: I tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2
[18:48:14] clustering.
[18:48:14] changing file names.

もとのファイル名に0〜9の番号をつけてクラスタリングされ、顔画像の多いもの、顔でない画像の多いものに分類されるので顔のみを抽出するのが少し楽になります。

ちなみに、転移学習せずにクラスタリングすることもできますが、転移学習をすると劇的にクラスタリングの精度が上がります。

何のためにこんなことするの?と思われる方が多いかもしれませんが、僕にはとても重要で便利な技術でした。

とりあえず、できた!