画像から顔を切り取ろう

###概要
OpenCVが用意しているHaar-Like特徴量Cascadeを使って顔の検出を行い、自動で切り取るプログラムを作りました。

###更新
このプログラムの[改良版](https://github.com/himazin331/Face-Cropping-NEW)を作成しました。
以前は１つの画像に複数個の顔が含まれていても１個しか切り取ることができなかったですが、改良版では全て切り取るようにしました。
その他、修正や機能追加をしましたので、こちらを利用することをおすすめします。

###環境
**-Software-**
Windows 10 Home
Anaconda3 64-bit(Python3.7)
Spyder
**-Library-**
opencv-python 4.1.2.30
natsort 7.0.0
**-Hardware-**
CPU: Intel core i9 9900K
RAM: 16GB 3200MHz

###参考
**書籍**
Pythonで始めるOpenCV4プログラミング 北山 直洋 (著) 
([Amazonページ](https://www.amazon.co.jp/Python%E3%81%A7%E5%A7%8B%E3%82%81%E3%82%8BOpenCV-4%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E5%8C%97%E5%B1%B1-%E7%9B%B4%E6%B4%8B/dp/4877834613))

###プログラム
Githubに上げておきます。

https://github.com/himazin331/Face-Cropping
リポジトリにはデータ加工プログラム、Haar-Cascadeが含まれています

###前提
本プログラムの動作にはHaar-Like特徴量のCascadeファイルが必須です。
今回はOpenCVのHaar-Cascadeを使用します。
なお、Cascadeはリポジトリに含まれるので別途用意する必要はありません。

###ソースコード
**コードが汚いのはご了承ください...**

```python:face_cut.py
﻿import cv2
import os
import argparse as arg
import sys
from natsort import natsorted


# 画像加工
def face_cut(imgs_dir, result_out, img_size, label, HAAR_FILE):
    # Haar-Like特徴量Cascade型分類器の読み込み
    cascade = cv2.CascadeClassifier(HAAR_FILE)

    # データ加工
    for img_name in natsorted(os.listdir(imgs_dir)):
        print("画像データ:{}".format(img_name))

        # jpg形式のみ
        _, ext = os.path.splitext(img_name)
        if ext.lower() == '.jpg':
            img_path = os.path.join(imgs_dir, img_name)  # ファイルパスの結合
            img = cv2.imread(img_path)  # データ読み込み

            img_g = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)  # グレースケールに変換
            face = cascade.detectMultiScale(img_g)  # 顔を検出

            # 顔が検出されなかったら
            if len(face) == 0:
                print("Face not found.")
            else:
                for x, y, w, h in face:
                    # 顔の切り取り
                    face_cut = img_g[y:y + h, x:x + w]
                    # リサイズ
                    face_img = cv2.resize(face_cut, (img_size, img_size))

                    # 保存
                    result_img_name = r'\data' + str(label) + '.jpg'
                    cv2.imwrite(os.path.join(result_out + result_img_name),
                                face_img)
                    label += 1

                print("Processing success!!")
        else:
            print("Unsupported file extension")


def main():
    # コマンドラインオプション作成
    parser = arg.ArgumentParser(description='Face image cropping')
    parser.add_argument('--imgs_dir', '-d', type=str, default=None,
                        help='画像フォルダパス(未指定ならエラー)')
    parser.add_argument('--out', '-o', type=str,
                        default=os.path.dirname(os.path.abspath(__file__)) + '/result_crop'.replace('/', os.sep),
                        help='加工後データの保存先(デフォルト値=./reslut_crop)')
    parser.add_argument('--img_size', '-s', type=int, default=32,
                        help='リサイズ(NxNならN,デフォルト値=32)')
    parser.add_argument('--label', '-l', type=int, default=1,
                        help='dataN.jpgのNの初期値(デフォルト値=1)')
    parser.add_argument('--haar_file', '-c', type=str,
                        default=os.path.dirname(os.path.abspath(__file__)) + '/haar_cascade.xml'.replace('/', os.sep),
                        help='haar-Cascadeのパス指定(デフォルト値=./haar_cascade.xml)')
    args = parser.parse_args()

    # 画像フォルダ未指定時->例外
    if args.imgs_dir is None:
        print("\nException: Cropping target is not specified.\n")
        sys.exit()
    # 存在しない画像フォルダ指定時->例外
    if os.path.exists(args.imgs_dir) is False:
        print("\nException: {} does not exist.\n".format(args.imgs_dir))
        sys.exit()
    # 存在しないCascade指定時->例外
    if os.path.exists(args.haar_file) is False:
        print("\nException: {} does not exist.\n".format(args.haar_file))
        sys.exit()

    # 設定情報出力
    print("=== Setting information ===")
    print("# Images folder: {}".format(os.path.abspath(args.imgs_dir)))
    print("# Output folder: {}".format(args.out))
    print("# Images size: {}".format(args.img_size))
    print("# Start index: {}".format(args.label))
    print("# Haar-cascade: {}".format(args.haar_file))
    print("===========================\n")

    # 出力フォルダの作成(フォルダが存在する場合は作成しない)
    os.makedirs(args.out, exist_ok=True)

    # 加工
    face_cut(args.imgs_dir, args.out, args.img_size, args.label, args.haar_file)
    print("")


if __name__ == '__main__':
    main()
```

###実行結果
用意した画像は以下の通り。
[大川竜弥](https://www.pakutaso.com/person/man/ookawa/)さんですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/32a0312c-576d-e31e-1104-ef1e3e69ce56.png "加工する画像")

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/89ed3582-2a24-3b43-3c93-97f02cabdd05.png "コンソール画面")

**「Face not found.」と出力されたものは、顔を検出できなかった、言い換えれば顔として認識できなかった**ことになります。
この例では、下の２つが顔として認識できませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/ac4fe7a2-8e0c-50ca-be8d-a8e5aaf8f65e.png "加工失敗")

どちらも顔が傾いています。ある程度の傾きなら問題ないのですが、画像のような角度で傾いているとダメみたいですね。どうしても顔を切り取りたい場合は、画像に対しアフィン変換をする必要があります。

この２つ以外はこのように、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/cfaf2459-ea30-1b7e-4e80-af2a514e2647.png "加工後の画像")

グレースケール化され、顔のみ切り取られてリサイズされています。
眼鏡をかけていても問題ありません。(サングラス、マスク着用は厳しいかも...)

**コマンド**
`python face_cut.py -d <画像フォルダ> (-o <保存先> -s <リサイズ> -l <インデックス> -c <cascade>)`

加工された画像データの保存先はデフォルトで`./result_crop`になっています。
Haar-cascadeの指定はデフォルトで`./haar_cascade.xml`となっています。
その他、リサイズは32×32px、インデックスは1がデフォルトで指定されています。

###説明
face_cut関数で、グレースケール化、顔の切り取り、リサイズをします。

まずは、顔を検出し、切り取るのに使うHaar-cascadeを読み込みます。

```python
    # Haar-Like特徴量Cascade型分類器の読み込み
    cascade = cv2.CascadeClassifier(HAAR_FILE)
```
`HAAR_FILE`はコマンドオプションより指定したHaar-cascadeのパスです。 

下にある処理では画像の読み込み、グレースケール化、顔の検出を行っています。

```python
    # データ加工
    for img_name in natsorted(os.listdir(imgs_dir)):
        print("画像データ:{}".format(img_name))

        # jpg形式のみ
        _, ext = os.path.splitext(img_name)
        if ext.lower() == '.jpg':
            img_path = os.path.join(imgs_dir, img_name)  # ファイルパスの結合
            img = cv2.imread(img_path)  # データ読み込み

            img_g = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)  # グレースケールに変換
            face = cascade.detectMultiScale(img_g)  # 顔を検出
```
今回は**JPEGファイルのみ対象**としていますが、
`if ext.lower() == '.jpg':`で`'.png'`や`'.bmp'`にすれば、そのファイルを対象に加工できます。

`cv2.cvtColor()`でRGB画像からグレースケール画像に変換します。
その後、`cascade.detectMultiScale()`でHaar-cascadeを用いた顔の検出を行います。

```python
            img_g = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)  # グレースケールに変換
            face = cascade.detectMultiScale(img_g)  # 顔を検出
```

`cascade.detectMultiScale()`は顔が検出されたとき、**検出箇所のx座標とy座標、幅と高さを返却**します。
顔が検出されなければなにも返却されません。

```python
            # 顔が検出されなかったら
            if len(face) == 0:
                print("Face not found.")
            else:
                for x, y, w, h in face:
                    # 顔の切り取り
                    face_cut = img_g[y:y + h, x:x + w]
                    # リサイズ
                    face_img = cv2.resize(face_cut, (img_size, img_size))

                    # 保存
                    result_img_name = r'\data' + str(label) + '.jpg'
                    cv2.imwrite(os.path.join(result_out + result_img_name),
                                face_img)
                    label += 1

                print("Processing success!!")
        else:
            print("Unsupported file extension")
```

下の処理では返される情報をもとに、顔部分の切り取りを行っています。

```python
                    # 顔の切り取り
                    face_cut = img_g[y:y + h, x:x + w]
```

下の図のような具合になっています。(見づらいですが...)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558214/83d6d332-277b-2978-fe46-71de3e737b4c.png "顔の切り取り")

切り取りができたら、`cv2.resize()`で指定のサイズにリサイズを行い、保存を行います。

以上です。
main関数については説明不要だと思うので割愛します。

###おわりに
私はデータスクレイピングとこのプログラムを使って、学習データの収集をしてました。
特段こだわりがなければ、十分使えると思います。

ただ、[実行結果](https://qiita.com/hima_zin331/items/c4160c5c31888e2066a4#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C)でも触れたとおり限界はあります。横顔の場合は横顔専用のHaar-cascadeも同梱されているので、そちらを使うといいかもしれません。
また、**誤検出も多少あるのでデータクレンジング**(顔ではない部分が切り取られた画像の排除)**が必要**です。