Webカメラとラズパイで収集した駐車場の画像に深層学習を利用して満空情報を識別してみようと思った話。

# はじめに

事務所の窓から正面にある駐車場の映像を５分ごとに撮影して動画を作って遊んでいます。

[Youtube - 週間 事務所の窓](https://www.youtube.com/playlist?list=PLExmrXpt_qyYRgbdiHpXZbpwZmrEWVUTz)

はじめの頃はそれだけで面白かったのですが、だんだんとつまらくなってきて「何か他にできることないかな〜」って考えていたら表題のようなことを思いついたので取り組んでみました。

また、この結果をWebサイトで常に確認できるようにしてます。

[駐車場 状態監視のサンプル](https://blueomega.jp)
※ 上述のとおり、写真は５分周期で更新されます。

# 使用するライブラリ

使用するライブラリをインストール。

## OpenCV

```
pip install -U pip
pip install python-opencv
```

## Tensorflow

```
pip install tensorflow
```

## Keras

```
pip install keras
```

# 準備

まず、以下のフォルダを作成。

```
├ img
│　├ 0-多い
│　├ 1-少ない
│　└ 2-ガラガラ
└ models
```

次に、img以下のそれぞれのフォルダに分類したい画像を保存。
今回はおよそ1週間分の画像データで、2019枚の画像を使用。

## 多い
![202010111320.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/95386078-0835-8faa-7d0b-196daf61b5cf.jpeg)
![202010110035.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/f57e0f7e-2801-d45a-8e95-43c6f2af6d1c.jpeg)
## 少ない
![202010111210.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/0835f7d8-d635-2c50-43bf-8da7de60c179.jpeg)
![202010170140.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/0873b6aa-144e-9bc7-bdda-4bff2ccf7b83.jpeg)
## ガラガラ
![202010120740.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/8781e854-0471-a4f6-3e59-6a1ac8d1a540.jpeg)
![202010112230.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/a027878c-4a16-0455-5a2c-a1c47dca2af5.jpeg)

# 学習

フォルダの分類にしたがって画像を学習。

```py:
# ライブラリ読込
import glob
import cv2
from matplotlib import pyplot as plt
import numpy as np

import keras
import tensorflow as tf

from sklearn import model_selection
from keras.utils.np_utils import to_categorical
from keras.layers import Activation, Conv2D, Dense, Flatten, MaxPooling2D, Dropout
from keras.models import Sequential

import random

# ラベルデータ作成
labels = []
for i in range(3):
    labels.append("{}-".format(i))
    
# 画像ファイル数の取得
n = []

for l in labels:
    files = glob.glob("img/{}*/*.jpg".format(l))
    print("{} : {}".format(l, len(files)))
    
    n.append(len(files))

# 画像ファイル読込
imgX = []
y = []

k = 0

for l in labels:
    
    print(l)
    
    files = glob.glob("img/{}*/*.jpg".format(l))
    files.sort()
    print(len(files), end=" -> ")
    
    # 使用する画像は最少分類区分の1.5倍まで
    j = int(min(n) * 1.5)
    if j > len(files):
        j = len(files)
    
    files = random.sample(files, j)
    print(len(files))
    
    i = 0

    for f in files:
        img = cv2.imread(f)
        h, w, c = img.shape
        # 上半分は車と関係ないので画像を加工
        img = img[int(h/2):h, :]
        img = cv2.resize(img, (100, 100))
        imgX.append(img)
        y.append(k)
        
        print("\r{}".format(i), end="")

        i += 1

    print()
    k += 1

# 画像データを配列データに変換
X = np.array(imgX)
X = X / 255

# 学習・検証用データに分割
test_size = 0.2

X_train, X_test, y_train, y_test = model_selection.train_test_split(X, y, test_size=test_size, random_state=42)
X_valid, X_test, y_valid, y_test = model_selection.train_test_split(X_test, y_test, test_size=.5, random_state=42)

y_train = to_categorical(y_train)
y_valid = to_categorical(y_valid)
y_test = to_categorical(y_test)

# 学習モデルの作成
input_shape = X[0].shape

model = Sequential()

model.add(Conv2D(
    input_shape=input_shape, filters=64, kernel_size=(5, 5), 
    strides=(1, 1), padding="same", activation='relu'))

model.add(MaxPooling2D(pool_size=(4, 4)))

model.add(Conv2D(
    filters=32, kernel_size=(5, 5), 
    strides=(1, 1), padding="same", activation='relu'))

model.add(Conv2D(
    filters=32, kernel_size=(5, 5), 
    strides=(1, 1), padding="same", activation='relu'))

model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(
    filters=16, kernel_size=(5, 5), 
    strides=(1, 1), padding="same", activation='relu'))

model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(1024, activation='sigmoid'))
model.add(Dense(2048, activation='sigmoid'))

model.add(Dense(len(labels), activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

# 学習
history = model.fit(
    X_train, y_train, batch_size=400, epochs=200, 
    verbose=1, shuffle=True,
    validation_data=(X_valid, y_valid))

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

# 学習済みモデルの保存
mdlname = "models/mdl_parking_status.h5"
model.save(mdlname)
```

途中の出力は割愛して、学習結果は以下のとおり。

```
validation loss:0.15308915078639984
validation accuracy:0.9653465151786804
```

学習の過程をグラフで確認すると以下のとおり。

![loss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/50754c8d-49d5-fe24-9282-43f0445d53fb.png)
![acc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/5b4edd6f-f217-36db-6847-45ab890efdd2.png)

なんとなく学習できてそう。

# 識別

以下のスクリプトで識別。

```py:
# ライブラリ読込
import glob
import cv2
from matplotlib import pyplot as plt
import numpy as np
import requests
import keras

# 学習済みモデルの読込
mdlname = "models/mdl_parking_status.h5"
model = keras.models.load_model(mdlname)

# ラベルデータの作成
labels = []

for lbl in glob.glob("img/*"):
    labels.append(lbl.split("/")[-1])

labels.sort()

# 画像読込
img_url = "https://map.blueomega.jp/parking/img.jpg"
req = requests.get(img_url)
img_org = np.fromstring(req.content, dtype='uint8')
img_org = cv2.imdecode(img_org, 1)

h, w, c = img_org.shape
img = img_org[int(h/2):h, :]
img = cv2.resize(img, (100, 100))
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

X = np.array([img])
X = X / 255

# 識別
pred = model.predict(X, batch_size=32)
m = np.argmax(pred[0])

# 結果表示
print(pred)
print(labels[m])
```

取得した画像はこちら。

![sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114240/7f6a8253-7a95-cd0e-3c0d-e3b69ab15df6.jpeg)

識別結果は以下のとおり。

```
[[9.2753559e-01 7.2361618e-02 1.0272356e-04]]
0-多い
```

...多分できた！