みらいテックラボ

音声・画像認識や機械学習など, 週末プログラマである管理人が興味のある技術の紹介や実際にトライしてみた様子などメモしていく.

コワーキングスペースの「混雑度」を検出する(1)

昨年友人より, 某コワーキングスペースの混雑度を見える化したいので, 機械学習部分を手伝ってほしいとの依頼があった.
そこで, 混雑度を測るために, 各スペースの人物検出を行い, 定員に対してどの程度の人がいるか検知することにした.


関連記事:
コワーキングスペースの「混雑度」を検出する(1)
コワーキングスペースの「混雑度」を検出する(2)
コワーキングスペースの「混雑度」を検出する(3)
コワーキングスペースの「混雑度」を検出する(4)


1. はじめに
コワーキングスペースは, 現状オープンスペースの場所と, 個別に区切られた場所などがある.

f:id:moonlight-aska:20210110230356j:plain:w400
f:id:moonlight-aska:20210110230419j:plain:w400

区切られた方は, カメラ位置変更しないと, 人物検出は難しそう!!

人物検出を行うにあたり, 以前の案件[1]ではYOLOv3[2]を使って行ったが, 今回はSSD(Single Shot MultiBox Detector)[3]を使ってみることにした.

KerasによるSSDの実装はいくつかあり, 以下あたりが有名である.
(1) rykov8/ssd_keras[4] : Keras v1.2.2, Tensorflow v1.0.0
(2) pierluigiferrari/ssd_keras[5] : Keras v2.x, Tensorflow v1.x
現状のKeras, Tensorflowのバージョンを考慮し, (2)の実装を使うこととした.

それにしても, 最近は論文のアルゴリズムを誰かが実装してgithubなどで公開してくれるので, 最新のアルゴリズムを使って何かをやろうという側にとってはなんとも便利な時代になったものだ...


2. データセット作成
学習データには, 上記スペースに設置したカメラで一定期間収集した画像を利用することにする.
とりあえず, 実験用に3週間分ほどのデータを提供いただいたので, アノテーションすることにした.
作業にあたり, 今回はアノテーションツールにLabelImg[6]を使用してみた.

[手順]
(1) 動画から一定時間毎にスナップショットを作成する.
(2) 同じような画像は除去する.
(3) アノテーションツールを使って, 人物領域とそのラベルを生成する.
  (※ 人物は椅子に座って作業している場合が多いので, 今回は上半身あたりを領域として指定するようにした.)

f:id:moonlight-aska:20210111005041j:plain:w500

[アノテーションデータの例]

<annotation>train_images</folder>
        <filename>Schedule-2020-12-YYYY.jpg</filename>
        <path>/home/aska/Project/XXX/train_images/Schedule-2020-12-YYYY.jpg</path>
        <source>
                <database>Unknown</database>
        </source>
        <size>
                <width>640</width>
                <height>480</height>
                <depth>3</depth>
        </size>
        <segmented>0</segmented>
        <object>
                <name>person</name>
                <pose>Unspecified</pose>
                <truncated>0</truncated>
                <difficult>0</difficult>
                <bndbox>
                        <xmin>503</xmin>
                        <ymin>159</ymin>
                        <xmax>593</xmax>
                        <ymax>261</ymax>
                </bndbox>
        </object>
        <object>
                <name>person</name>
                <pose>Unspecified</pose>
                <truncated>0</truncated>
                <difficult>0</difficult>
                <bndbox>
                        <xmin>399</xmin>
                        <ymin>3</ymin>
                        <xmax>443</xmax>
                        <ymax>55</ymax>
                </bndbox>
        </object>
</annotation>

一応, 学習データ336枚(837領域), 評価データ67枚(178領域)を準備した.


3. モデル学習
モデルの学習には, ssd_keras/ssd300_training.ipynbを少し修正して使用した.
主な変更箇所:

主なライブラリのバージョン:

  • Keras 2.2.2
  • Tensorflow 1.10.1


4. 人物検出
学習済モデルを使って, 人物検出をやってみた.

[コード]

import numpy as np
import os
import sys
import cv2
import argparse

from imageio import imread
sys.path.append('./ssd_keras')
from models.keras_ssd300 import ssd_300
from keras_loss_function.keras_ssd_loss import SSDLoss
from keras.optimizers import Adam
from keras.preprocessing import image

MODEL_DIR = './trained_models'
MODEL_NAME = 'ssd300_person_epoch-10_loss-2.7150_val_loss-2.9814.h5'

classes = ['background',
           'person']

CONFIDENCE_THRESHOLD = 0.50
# Set the image size.
IMG_HEIGHT = 300
IMG_WIDTH = 300

def load_model():
    global model
    model = ssd_300(image_size=(IMG_HEIGHT, IMG_WIDTH, 3),
                  n_classes=1,
                  mode='inference',
                  l2_regularization=0.0005,
                  scales=[0.1, 0.2, 0.37, 0.54, 0.71, 0.88, 1.05],
                  aspect_ratios_per_layer=[[1.0, 2.0, 0.5],
                                         [1.0, 2.0, 0.5, 3.0, 1.0/3.0],
                                         [1.0, 2.0, 0.5, 3.0, 1.0/3.0],
                                         [1.0, 2.0, 0.5, 3.0, 1.0/3.0],
                                         [1.0, 2.0, 0.5],
                                         [1.0, 2.0, 0.5]],
                  two_boxes_for_ar1=True,
                  steps=[8, 16, 32, 64, 100, 300],
                  offsets=[0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
                  clip_boxes=False,
                  variances=[0.1, 0.1, 0.2, 0.2],
                  normalize_coords=True,
                  subtract_mean=[123, 117, 104],
                  swap_channels=[2, 1, 0],
                  confidence_thresh=0.5,
                  iou_threshold=0.45,
                  top_k=200,
                  nms_max_output_size=400)
    # load weight
    print(model.summary())
    weights_path = os.path.join(MODEL_DIR, MODEL_NAME)
    model.load_weights(weights_path, by_name=True)
    adam = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
    ssd_loss = SSDLoss(neg_pos_ratio=3, alpha=1.0)
    model.compile(optimizer=adam, loss=ssd_loss.compute_loss)
    print('Model loading completed')

# 最終結果をJSON形式で格納
def set_results(y_pred, fname):
    res = [y_pred[k][y_pred[k,:,1] > CONFIDENCE_THRESHOLD] for k in range(y_pred.shape[0])]
    img = cv2.imread(fname)
    data = []
    for r in res[0]:
        left = int(r[2] * img.shape[1] / IMG_WIDTH)
        top = int(r[3] * img.shape[0] / IMG_HEIGHT)
        right = int(r[4] * img.shape[1] / IMG_WIDTH)
        bottom = int(r[5] * img.shape[0] / IMG_HEIGHT)
        bbox = {'left' : str(left), 'bottom' : str(bottom), 'right' : str(right), 'top': str(top)}
        data.append({'label': classes[int(r[0])], 'score': str(r[1]), 'bbox': bbox})
        cv2.rectangle(img, (int(left), int(top)), (int(right), int(bottom)), (0, 0, 255))
    cv2.imshow('img', img)
    return data

def main(args):
    input_images = [] # Store resized versions of the images here.
    img = image.load_img(args.input, target_size=(IMG_HEIGHT, IMG_WIDTH))
    img = image.img_to_array(img) 
    input_images.append(img)
    input_images = np.array(input_images)
    # SSD処理
    y_pred = model.predict(input_images)
    res = set_results(y_pred, args.input)
    print(res)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
        
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    help_ = 'Image file'
    parser.add_argument('-i', '--input', help=help_)
    args = parser.parse_args()
    load_model()
    main(args)

[実行例]
f:id:moonlight-aska:20210111111931j:plain:w400

人物検出が一応動作するようになった.
コワーキングスペースの混雑度は, 5分や10分毎でよいのでクラウド上での動作でよいかと思うが, AI展示的なこともやりたいということで, 最終的にはNvidiaのJetson Nanoで動かすことになっている.

----
参照URL:
[1] ピープルカウンタを考えてみる(1)~(8)
[2] YOLO: Real-Time Object Detection
[3] SSD: Single Shot MultiBox Detector
[4] rykov8/ssd_keras: Port of Single Shot MultiBox Detector to Keras
[5] pierluigiferrari/ssd_keras: A Keras port of Single Shot MultiBox ...
[6] tzutalin/labelImg: LabelImg is a graphical image annotation tool ann...