新生児のstateを判別するアプリ

# はじめに
AidemyのAIアプリ開発コースで学習した総仕上げとして、新生児のstate(表情・状態)を判別するアプリを作成しました。
私は元々看護師としてNICU（新生児集中治療室）で働いており、赤ちゃんに着目し看護を行うなかで、赤ちゃんの覚醒状態に合わせてケアを行う大切さを感じていました。stateは赤ちゃんのことを理解する上でとても大事な指標だと思い、今回画像認識で赤ちゃんのstateを判別できるアプリを作成しました！
***
補足として
赤ちゃんのstateの指標です。
「state1」は「深い睡眠」（静的睡眠）。ぐっすりと寝ている状態。
「state2」は「浅い睡眠」（動的睡眠）。起きるか眠るか迷っているような状態のことで、時に笑ったりぐずり泣きをしたりもする。
「state3」は「まどろみ」。起きている状態よりは、ぼうっとしていて反応が遅いような状態。
「state4」は「覚醒」（静的覚醒）。明るく目覚めた状態で何かを注目して見たりする。ちなみに授乳に適した表情は「state3、4」の「まどろみ～覚醒」の状態であると言われている。
「state5」は「ぐずり」。ぐずって声を出したり活発に表情を変えたりする。空腹や疲れ、不快などに敏感。
「state6」は「啼泣」。号泣しており、コントロールが難しい状態。
***
# 目次
[はじめに](#はじめに)
[目的](#目的)
[実行環境](#実行環境)
[実装内容１](#実装内容１)
[考察１](#5考察１)
[実装内容２](#実装内容２)
[考察２](#3考察２)
[実装内容３](#実装内容３)
[考察３](#4考察３)
[感想](#感想)

# 目的
赤ちゃんのstateは１〜６までありますが、実際には呼吸の状態や動きも併せてみており画像だけでは判別できないところも含まれているので、今回は①state1.2 ②state3.4 ③state5.6　3つの状態に分けて判別したいと思います。
①は寝ている状態（sleeping）②は起きている状態（normal）③泣いている状態（crying）で分けています。
ゆくゆくは動作や状態も判別できるようになりたいです、、、

# 実行環境
python-3.8.16
Google Colaboratory

# 実装内容１
## 1.画像の収集
ネットで調べてみるとicrawlerを使うとはやくデータが集められるとのことだったので使用してみました。
```python
!pip install icrawler

#pythonライブラリの「icrawler」でBing用モジュールをインポート
from icrawler.builtin import BingImageCrawler

#ダウンロードするキーワード
search_word = "新生児　泣いている"
crawler = BingImageCrawler(storage={'root_dir': "/content/drive/MyDrive/baby crying"})

#ダウンロードする画像の最大枚数は300枚
crawler.crawl(keyword=search_word, max_num=300)
```
これを使うと簡単にダウンロードはできましたが、余計な画像が多かったので、削除に時間がかかりました。またチューターさんにそれぞれ100枚以上は画像があったほうがいいと言われていたので、足りない枚数は自分でダウンロードしました。
なので、①sleeping、②normal、③cryingはそれぞれ100枚集めました！
![スクリーンショット 2023-02-05 17.56.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124840/cb86a1b4-e07d-b930-7143-bc4858b7016f.png)
このような感じに集めることができましたが、赤ちゃんの顔がドアップのものもあれば身体も込みで写っているものもあります。これで正確に判断されるのか気になりますがとりあえず進めてみます！

## 2.必要なモジュールのインポートと画像データの読み込み
下記のコードで今回のアプリに必要なモジュールをインポートします。
```python
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
from google.colab import drive
drive.mount('/content/drive')
```
画像を読み込む際に、画像サイズを縦横200X200になるように加工して、格納します。
配列に格納した画像をランダムに並び替えます。
データを学習用データ80%、検証用データ20%に分割していきます。
one-hotはカテゴリカル（質的）データを0又は1で表現した変数のことです。今回３種類に分類するので、例えば①normalは[1,0,0]となります。
```python
img_normal = "./drive/MyDrive/normal/"
img_crying = "./drive/MyDrive/crying/"
img_sleeping = "./drive/MyDrive/sleeping/"
#os.listdir() で指定したファイルを取得
path_normal = os.listdir(img_normal)
path_crying = os.listdir(img_crying)
path_sleeping = os.listdir(img_sleeping)
#画像を格納するリスト作成
img_normal_list = []
img_crying_list = []
img_sleeping_list =[]

for i in range(len(path_normal)):
    img = cv2.imread(img_normal + path_normal[i])#画像の読み込み
    img = cv2.resize(img, (200,200))#画像のリサイズ
    img_normal_list.append(img)#画像配列に画像を追加

for i in range(len(path_crying)):
    img = cv2.imread(img_crying + path_crying[i])
    img = cv2.resize(img, (200,200))
    img_crying_list.append(img)

for i in range(len(path_sleeping)):
    img = cv2.imread(img_sleeping + path_sleeping[i])
    img = cv2.resize(img, (200,200))
    img_sleeping_list.append(img)

#np.arrayでXに学習画像、ｙに正解ラベルを代入
X = np.array(img_normal_list + img_crying_list + img_sleeping_list)
y =  np.array([0]*len(img_normal_list) + [1]*len(img_crying_list) + [2]*len(img_sleeping_list))
#配列のラベルをシャッフルする
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]
#学習データと検証データを用意
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]
#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
```
## 3.モデルの定義
最終層を除くVGG16を利用して、これに自分で構築したモデルを接続して一つのモデルにします。
```python
#転移学習のモデルとしてVGG16を使用
input_tensor = Input(shape=(200, 200, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(512, activation="relu"))
top_model.add(Dense(128, activation='relu'))
top_model.add(Dense(64, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(3, activation='softmax'))

#VGG16とtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
#VGG16による特徴抽出部分の重みを15層までに固定
for layer in model.layers[:15]:
   layer.trainable = False

model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=['accuracy'])
```

## 4.モデルの学習と精度の評価

```python
history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test))
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))
 #acc, val_accのプロット
plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
```
![スクリーンショット 2023-02-05 18.26.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124840/9a629f55-c898-ff72-4db3-b43904bf473d.png)

サイズを調整したり、epochs数を変えてみることしかできず、色々試しましたが、正解率は84％となりました。

## 5.考察１
epochsが30のところで正解率が横ばいになってきていることや今回画像が各100枚ずつしか集められなかったことによる学習不足があったので今回の結果は過学習の可能性も考えられます。
また、元々赤ちゃんの顔の向きが違うことや身体も画像に入っているものが気になっていたので顔検出で赤ちゃんの顔のみを切り取って、学習させたいと思います。

#  実装内容２
実装内容１の画像収集でも言っている通り、赤ちゃんの顔がドアップになっている写真や身体全体が入っている写真などばらつきがあり、そこがどう影響しているのか気になっていたので、画像の顔検出を行い、検証したいと思います！

## 1.画像の編集（顔検出）
画像から顔を切り抜くのはOpenCVを使用しました。

```python
import os
import sys
import glob
import time
import cv2
import numpy as np

# 画像サイズ
image_size = 100
# カスケード分類器
HAAR_FILE = "/content/drive/MyDrive/images/haarcascade_frontalface_default.xml"
cascade = cv2.CascadeClassifier(HAAR_FILE)

newborns_state = ["normal","crying","sleeping"]
for newborn_state in newborns_state:

    # 入力する画像ファイルのディレクトリ
    IMAGE_DIR = "/content/drive/MyDrive/images/{}/".format(newborn_state)
    # 顔の切り抜きが成功した時に出力するディレクトリ
    file_path_1 = "/content/drive/MyDrive/image_face/{}/".format(newborn_state)
    # 顔の切り抜きが失敗した時に出力するディレクトリ
    file_path_2 = "/content/drive/MyDrive/image_face_false/{}/".format(newborn_state)

    # 画像の読み込み
    file_list = glob.glob("{}*.jpg".format(IMAGE_DIR))
    for i, filename in enumerate(file_list):
        img = cv2.imread(filename)
        img_gray = cv2.imread(filename, 0)

        # 画像が読み込めない場合
        if img is None:
            print(filename)
            print()

        # 顔の切り抜きを実行し、画像ファイルを保存
        try:
            face = cascade.detectMultiScale(img_gray)
            j = 0
            for x, y, w, h in face:
                face_cut = img[y:y+h, x:x+w]
                face_cut = cv2.resize(face_cut, (image_size, image_size))
                os.makedirs(file_path_1, exist_ok=True)
                cv2.imwrite(file_path_1 + "{}_{}_{}.jpg".format(newborn_state, str(i).zfill(3), str(j).zfill(3)), face_cut)
                j += 1

        # 顔の切り抜きができなかった時はそのまま保存
        except Exception:
            os.makedirs(file_path_2, exist_ok=True)
            cv2.imwrite(file_path_2 + "{}_{}.jpg".format(newborn_state, str(i).zfill(3)), filename)
```
結果、自動的に顔検出できたのは
①寝ている状態（sleeping）　1枚
②起きている状態（normal）　６枚
③泣いている状態（crying）　20枚
でした、、。

自動検出できなかった分は手動で切り抜きを行い、全て顔を切り抜くことができました！

## 2.モデルの学習と精度の評価
必要なモジュールのインポートと画像データの読み込みに関しては実装内容①と一緒です。
今回顔検出を行った後に、コードの実行を行うと、はじめの損失関数からnanと表示されました。
チューターに相談し、データの不具合がないかなどを確認してもらいデータの問題はなく、おそらくモデルのところに原因があるだろうということで、活性化関数のreluをsigmoidに変更したら損失関数がちゃんと表示されました！以下がsigmoid関数に変更したコードです！

```python
#転移学習のモデルとしてVGG16を使用
input_tensor = Input(shape=(200, 200, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(512, activation="sigmoid"))
top_model.add(Dense(128, activation="sigmoid"))
top_model.add(Dense(64, activation="sigmoid"))
top_model.add(Dropout(0.5))
top_model.add(Dense(3, activation='softmax'))

#VGG16とtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
#VGG16による特徴抽出部分の重みを15層までに固定
for layer in model.layers[:15]:
   layer.trainable = False

model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=['accuracy'])
```
正解率は98％となりました。結構良い数値が出て驚きました！
![スクリーンショット 2023-02-17 16.36.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124840/58199986-bf00-9b7f-e966-9083bb47a47e.png)

## 3.考察２
顔の切り抜きをするだけでとても正解率が上がりました。
結果としてはいい結果ですが、実行するたびに正解率が数パーセント違ってくるので、画像の枚数を増やしたり、モデルの層の数やエポック数の調整を行ったりしながら、安定して良い結果が出るよう改善する必要がありそうです。

# 実装内容３
次は画像の水増しを行い、精度の評価を行いたいと思います！

## 1.画像を訓練データとテストデータに分割
画像はそれぞれ約100枚ずつあったので、３割の30枚をテストデータとして手動で分割しました。

## 2.訓練データの拡張
残りの約70枚を訓練データとして水増ししていきます。
データクレンジングを使用して
・左右反転
・閾値処理
・ぼかし
の処理を行い、水増ししました。

```python
import os
import glob
import numpy as np
import matplotlib.pyplot as plt
import cv2
from google.colab import drive
drive.mount('/content/drive')
# 左右反転の水増しのみ使用
def scratch_image(img, flip=True, thr=True, filt=True, resize=False, erode=False):
    # 水増しの手法を配列にまとめる
    methods = [flip, thr, filt, resize, erode]

    # flip は画像の左右反転
    # thr  は閾値処理
    # filt はぼかし
    # resizeはモザイク
    # erode は縮小
    #     をするorしないを指定している
    # 
    # imgの型はOpenCVのcv2.read()によって読み込まれた画像データの型
    # 
    # 水増しした画像データを配列にまとめて返す

    # 画像のサイズを習得、ぼかしに使うフィルターの作成
    img_size = img.shape
    filter1 = np.ones((3, 3))

    # オリジナルの画像データを配列に格納
    images = [img]

    # 手法に用いる関数
    scratch = np.array([
        #画像の左右反転のlambda関数を書いてください
        lambda x: cv2.flip(x, 1),
        #閾値処理のlambda関数を書いてください
        lambda x: cv2.threshold(x, 100, 255, cv2.THRESH_TOZERO)[1],  
        #ぼかしのlambda関数を書いてください
        lambda x: cv2.GaussianBlur(x, (5, 5), 0),
        #モザイク処理のlambda関数を書いてください
        lambda x: cv2.resize(cv2.resize(x,(img_size[1]//5, img_size[0]//5)), (img_size[1], img_size[0])),
        #縮小するlambda関数を書いてください
        lambda x: cv2.erode(x, filter1)  
    ])

    # 関数と画像を引数に、加工した画像を元と合わせて水増しする関数
    doubling_images = lambda f, imag: (imag + [f(i) for i in imag])

    # doubling_imagesを用いてmethodsがTrueの関数で水増ししてください

    for func in scratch[methods]:
        images = doubling_images(func,  images)

    return images

# state名
#state = "crying"
#state = "normal"
state = "sleeping"

# stateの画像フォルダのパス
path ="/content/drive/MyDrive/image_face/sleeping"  # 画像のパス

# stateの画像フォルダの中の全画像のパスを取得して配列化
img_path_list = glob.glob(path + "/*")
print(len(img_path_list))

for img_path in img_path_list:
    # 画像ファイル名を取得
    base_name = os.path.basename(img_path)
    print(base_name)
    # 画像ファイル名nameと拡張子extを取得
    name,ext = os.path.splitext(base_name)
    print(name + ext)

    # 画像ファイルを読み込む
    img = cv2.imread(img_path, 1)
    scratch_images = scratch_image(img)

    # 画像保存用フォルダ作成
    if not os.path.exists(state + "_train_images"):
        os.mkdir(state + "_train_images")

    for num, im in enumerate(scratch_images):
        # まず保存先のディレクトリ"train_images/"を指定、番号を付けて保存
        cv2.imwrite(state + "_train_images/" + name + str(num) + ext ,im) 
        
```
それぞれ約70枚だったのが、約560枚まで水増しすることができました。

## 3.モデルの学習と精度の評価
訓練データとテストデータをリストに代入してして、実装内容①、②と同様に転移モデルVGG16と接続して一つのモデルにします。
```python
import os, glob
import random
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import load_model, save_model
from tensorflow.keras.preprocessing.image import ImageDataGenerator, img_to_array, load_img
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras import optimizers, Model, Sequential
#from google.colab import drive
#drive.mount('/content/drive')
#from google.colab import files

states = ["normal","crying","sleeping"]
IMAGE_DIR_TRAIN = "/content/drive/MyDrive/image_face/train_images"
IMAGE_DIR_TEST = "/content/drive/MyDrive/image_face/test_images"

num_classes = len(states)
image_size = 100

# 訓練データとテストデータをわける
X_train = []
X_test  = []
y_train = []
y_test  = []

# 訓練データをリストに代入
for index, state in enumerate(states):
    files = glob.glob(os.path.join(IMAGE_DIR_TRAIN, state + "/*"))
    for file in files:
        image = load_img(file)
        image = image.resize((image_size, image_size))
        image = img_to_array(image)
        X_train.append(image)
        y_train.append(index)

# テストデータをリストに代入
for index, state in enumerate(states):
    files = glob.glob(os.path.join(IMAGE_DIR_TEST, state + "/*"))
    for file in files:
        image = load_img(file)
        image = image.resize((image_size, image_size))
        image = img_to_array(image)
        X_test.append(image)
        y_test.append(index)

# テストデータと訓練データをシャッフル
p = list(zip(X_train, y_train))
random.shuffle(p)
X_train, y_train = zip(*p)

q = list(zip(X_test, y_test))
random.shuffle(q)
X_test, y_test = zip(*q)

# Numpy配列に変換
X_train = np.array(X_train)
X_test = np.array(X_test)
y_train = np.array(y_train)
y_test = np.array(y_test)

# データの正規化
X_train = X_train / 255.0
X_test = X_test / 255.0

# One-hot表現
y_train = to_categorical(y_train, num_classes)
y_test = to_categorical(y_test, num_classes)

#転移学習のモデルとしてVGG16を使用
input_tensor = Input(shape=(100,100, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation="sigmoid"))
top_model.add(Dense(128, activation="sigmoid"))
top_model.add(Dense(64, activation="sigmoid"))
top_model.add(Dropout(0.5))
top_model.add(Dense(3, activation='softmax'))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:15]:
   layer.trainable = False

model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=['accuracy'])

history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test))
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))

model.save("/content/drive/MyDrive/image_face/model.h5")

 #acc, val_accのプロット
plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
```

## 4.考察３
正解率95％となりました。結果的にいい数値のものができましたが、実際のアプリを動かしてみると、若干sleepingの正解率が低く感じ、まだまだ改良が必要だと感じました。
各500〜600枚の学習データしかないので、データ不足もありそうです。
![スクリーンショット 2023-03-10 19.00.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124840/aa3aab6e-574d-f2c4-7e56-7a7e9e8a9dcd.png)

## 感想
実際アプリを作成するのはとても難しく理解が追いついていない部分の方が多いです、、。
ですが、チューターさんにたくさん質問して少しづつ理解していくことができました。
まだまだ勉強不足なので、これからも少しづつ勉強して、できることを増やしていきたいと思いました！

アプリのURLです！
https://newbornstate-app.onrender.com
（本当は判別されたstateに合ったコメントを表示できるようにしたかったのですが、今回はHTMLとCSSの勉強不足でできませんでした、、また次回トライします）