夏休みなので小学生とPythonを勉強してみた - 課題10 顔の認識

小学6年生の長男が夏休みの自由研究にPythonを勉強したいというので付き合ってみました。
どこから手を付けていけばわからないので、いくつか課題を出し、それを実現するコードを一緒に書くということを繰り返しました。

この記事では、「課題10 顔の認識」について扱います。その他の課題については下記の記事をご覧ください。

* [夏休みなので小学生とPythonを勉強してみた - 課題1 FizzBuzz - Qiita](https://qiita.com/shakiyam/items/b8366fde704639abb63d)
* [夏休みなので小学生とPythonを勉強してみた - 課題2 掛け算九九 - Qiita](https://qiita.com/shakiyam/items/0e4153fe7c610b3e8e49)
* [夏休みなので小学生とPythonを勉強してみた - 課題3 掛け算ゲーム - Qiita](https://qiita.com/shakiyam/items/4b4df04369914df3baa6)
* [夏休みなので小学生とPythonを勉強してみた - 課題4 じゃんけん - Qiita](https://qiita.com/shakiyam/items/05c756b4726c941c1c5c)
* [夏休みなので小学生とPythonを勉強してみた - 課題5 サイコロの目の出る確率 - Qiita](https://qiita.com/shakiyam/items/9e0c5548efd0952ab250)
* [夏休みなので小学生とPythonを勉強してみた - 課題6 写真の整理 - Qiita](https://qiita.com/shakiyam/items/2dd2e2763ccf85b19126)
* [夏休みなので小学生とPythonを勉強してみた - 課題7 掛け算ゲームWeb版 - Qiita](https://qiita.com/shakiyam/items/3945a224a8774c332220)
* [夏休みなので小学生とPythonを勉強してみた - 課題8 掲示板 - Qiita](https://qiita.com/shakiyam/items/c3aa6425ff42b75d3310)
* [夏休みなので小学生とPythonを勉強してみた - 課題9 顔の検出 - Qiita](https://qiita.com/shakiyam/items/110b4819b12124587d80)

コードは[GitHub](https://github.com/shakiyam/python4kids)にて公開しています。

課題10-1 顔画像の分割
---------------------

### 指示 ###

顔画像の集合をトレーニング用とテスト用に分割するプログラムを作りましょう。
./srcにある顔画像を、./trainおよび./testにランダムに格納します。

### 回答例 ###

```python
import glob
import random
import shutil

for image_path in glob.glob('./src/*/*.jpg'):
    if random.random() <= 0.8:
        shutil.copy(image_path, image_path.replace('./src/', './train/', 1))
    else:
        shutil.copy(image_path, image_path.replace('./src/', './test/', 1))
```

課題10-2 顔の認識
-----------------

### 指示 ###

顔の認識を行うプログラムを作りましょう。
./trainにある顔画像を学習し、./testにある顔画像を認識させます。./trainおよび./test以下の顔画像が格納されているディレクトリの名前は「ラベル_ラベル名」という形式とします。ラベルは数値です。

### ヒント ###

OpenCVには顔認識のモジュールが提供されています。

* [OpenCV: Face Recognition with OpenCV](https://docs.opencv.org/4.1.1/da/d60/tutorial_face_main.html)

今回はLocal Binary Patterns Histogramsというアルゴリズムを使います。トレーニング用顔画像をグレーに変換し、サイズをそろえてラベルと与えて学習させます。テスト用顔画像も同様にグレーに変換し、サイズをそろえて結果を取得します。結果には推測されたラベルと、その確からしさ(0に近いほ良い)が含まれます。

### 回答例 ###

```python
import glob
import os

import cv2

import numpy


def get_gray_resized_image(image_path):
    image = cv2.imread(image_path)
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray_resized_image = cv2.resize(gray_image, (500, 500))
    return gray_resized_image


def get_label(path):
    return int(path.split(os.sep)[2].split('_')[0])


train_images = []
train_labels = []
for image_path in glob.glob('./train/*/*.jpg'):
    train_images.append(get_gray_resized_image(image_path))
    train_labels.append(get_label(image_path))
recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.train(train_images, numpy.array(train_labels))

for image_path in glob.glob('./test/*/*.jpg'):
    image_name = os.path.basename(image_path)
    gray_resized_image = get_gray_resized_image(image_path)
    predicted_label, confidence = recognizer.predict(gray_resized_image)
    image_label = get_label(image_path)
    print(f'Image: {image_name}', end='\t')
    print(f'Predicted: {predicted_label}', end='\t')
    print(f'Confidence: {confidence:3.1f}', end='\t')
    print(f'Anser: {image_label}')
```

### 実行例 ###

```console
Image: DSC_6322_1023-1776.jpg   Predicted: 2    Confidence: 28.3        Anser: 1
Image: DSC_6658_1160-566.jpg    Predicted: 2    Confidence: 41.0        Anser: 1
Image: IMG_3036_895-622.jpg     Predicted: 1    Confidence: 22.1        Anser: 1
Image: IMG_3663_2257-1166.jpg   Predicted: 2    Confidence: 33.7        Anser: 1
Image: IMG_3770_1708-619.jpg    Predicted: 2    Confidence: 24.9        Anser: 1
Image: IMG_5088_780-1565.jpg    Predicted: 1    Confidence: 36.5        Anser: 1
Image: IMG_3663_1220-1431.jpg   Predicted: 2    Confidence: 30.8        Anser: 2
Image: IMG_3671_158-917.jpg     Predicted: 1    Confidence: 19.8        Anser: 2
Image: IMG_3772_774-981.jpg     Predicted: 1    Confidence: 54.6        Anser: 2
Image: IMG_4337_1521-1070.jpg   Predicted: 2    Confidence: 36.5        Anser: 2
Image: IMG_5085_1185-1571.jpg   Predicted: 2    Confidence: 36.7        Anser: 2
Image: IMG_6555_1260-2329.jpg   Predicted: 2    Confidence: 16.0        Anser: 2
Image: IMG_6707_1411-653.jpg    Predicted: 2    Confidence: 24.1        Anser: 2
```

課題10-3 顔の認識結果をHTML出力
-------------------------------

### 指示 ###

顔の認識を行い、結果をHTMLで出力するプログラムを作りましょう。

### ヒント ###

Flaskで使っていたJinja2は単体でも使えます。

* [API — Jinja Documentation](https://jinja.readthedocs.io//api/)

リストの内包表記は辞書にも使えます。

* [5.1.3. リストの内包表記 — Python チュートリアル](https://docs.python.org/ja/3/tutorial/datastructures.html#list-comprehensions)

### 回答例 ###

```python
import glob
import os

import cv2

from jinja2 import Environment, FileSystemLoader, select_autoescape

import numpy


def get_gray_resized_image(image_path):
    image = cv2.imread(image_path)
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray_resized_image = cv2.resize(gray_image, (500, 500))
    return gray_resized_image


def get_label(path):
    return int(path.split(os.sep)[2].split('_')[0])


def get_label_name(path):
    return path.split(os.sep)[2].split('_')[1]


label_and_names = {
    get_label(path): get_label_name(path) for path in glob.glob('./src/*')
}

train_images = []
train_labels = []
for image_path in glob.glob('./train/*/*.jpg'):
    train_images.append(get_gray_resized_image(image_path))
    train_labels.append(get_label(image_path))
recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.train(train_images, numpy.array(train_labels))

test_results = []
for image_path in glob.glob('./test/*/*.jpg'):
    image_name = os.path.basename(image_path)
    gray_resized_image = get_gray_resized_image(image_path)
    predicted_label, confidence = recognizer.predict(gray_resized_image)
    test_results.append(
        (image_path,
         label_and_names[predicted_label],
         confidence,
         get_label_name(image_path))
    )

env = Environment(
    loader=FileSystemLoader('./templates'),
    autoescape=select_autoescape()
)
template = env.get_template('02.html')
template.stream(test_results=test_results).dump('output.html')
```

```html:02.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Face Recognition</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <div class="container">
        <h1 class="navbar-brand">Face Recognition</h1>
      </div>
    </nav>
    <div class="container">
      <div class="list-group">
        {% for image_path, predicted_label, confidence, image_label in test_results %}
          <div class="list-group-item">
            <div class="d-flex flex-row">
              <div class="p-1 bd-highlight">
                  <img src="{{ image_path }}" width="100px" height="100px">
              </div>
              <div class="p-1 bd-highlight">
                <div class="d-flex flex-column">
                  {% if predicted_label == image_label %}
                    <div>Predicted: <span style="color:green">{{ predicted_label }}</span></div>
                  {% else  %}
                    <div>Predicted: <span style="color:red">{{ predicted_label }}</span></div>
                  {% endif %}
                  {% if confidence < 30 %}
                    <div>Confidence: <span style="color:green">{{ '%3.1f'|format(confidence) }}</span></div>
                  {% else  %}
                    <div>Confidence: <span style="color:red">{{ '%3.1f'|format(confidence) }}</span></div>
                  {% endif %}
                  <div>Answer: {{ image_label }}</div>
                </div>
              </div>
            </div>
          </div>
        {% endfor %}
      </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>
```

### 実行例 ###

![出力されたHTML](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174/ed7a01e4-5cb6-95e0-8a10-3f308b881992.png)