機械学習で共有スペース利用状況の解析


きっかけ

2017/05/08に僕が勤務するgifteeオフィスが移転しました。

新しいオフィスではソファー席やファミレス席など、
いろいろなタイプの共有スペースが増えました。
https://www.wantedly.com/companies/giftee/post_articles/64703

そこで、どのタイプの共有スペースがどれくらい利用されているか把握したい
との要望があがってきたため、利用状況の取得方法について検討することにしました。

検討

利用状況の把握に人感センサーや、感圧センサーなどの使用も候補にあがりましたが、
人感センサーでは人数まではとれなさそうなこと、
感圧センサーでは席ごとにセンサーが必要になってしまうため、
共有スペース毎に定期的に写真を撮影して、そこに写っている人数を計測する方法にしました。

人数解析方法

画像から人数の取得については機械学習フレームワークを使用することにしました。

今回は
・ 学習済みデータが公開されている
・ 利用方法が簡単
の二点からdarknetを使うことにしました。

darknet

インストール

インストールはとても簡単で、githubからcloneしてmakeを実行するだけです。

git clone https://github.com/pjreddie/darknet
cd darknet
make

これに学習済みの重みデータをダウンロードすれば準備完了です。

wget https://pjreddie.com/media/files/yolo.weights

画像の解析

darknetのオプションにdetectを指定して、config、重みデータ、対象の写真を渡します。
ソースに含まれているdata/person.jpgに対して解析を行う場合は以下のようになります。

./darknet detect cfg/yolo.cfg yolo.weights data/person.jpg

結果は同階層に「predictions.png」の名前で出力されます。

元画像

解析後の画像

人数取得方法

darknetの解析結果は標準出力にも以下のように出力されます。

data/person.jpg: Predicted in 14.067749 seconds.
person: 86%
horse: 82%
dog: 86%

今回は単純に標準出力のpersonをgrepしてそのカウントを取る方式で、画像に写っている人数を取得しました。

./darknet detect cfg/yolo.cfg yolo.weights data/person.jpg | grep person | wc -l

システムの全体の流れ

今回作ったシステムは大きく3つのフェーズに分かれます。

1.iPhoneで定期間隔で対象エリアを撮影し、S3にアップロード
2.解析スクリプトを定期的にcronで動かし、写真に写った人数を取得
 取得できた人数をDynamoDBにinsert
3.re:dashでDynamoDBのデータを視覚化

フェーズ1

@koh518 作のiPhoneアプリで一定間隔で共有スペースを撮影します。(今回は5分おき)
iPhoneを固定する台がなかったため、マグカップに入れて固定化しました。

撮影された画像はS3にアップロードされます。
S3のキー名は「共有スペース名/時間.jpeg」(e.g dining/20170620131500.jpeg)としました。

フェーズ2

cronから起動された解析スクリプトが、S3に溜まっている画像を順番に取りだし、人数の解析を実施します。
ただ、darknetではjpegのexifのorientation情報が認識されないため、
ImageMagickのconvertコマンドで正しい向きに変換しておきます。

convert iphone.jpg -auto-orient converted.png

こうしないと横向きで解析され精度がかなり悪くなってしまいました。

次に変換した画像をdarknetに渡してpersonのカウントを取得します。

./darknet detect cfg/yolo.cfg yolo.weights ../converted.png | grep person | wc -l

画像は以下のように解析されました。

元画像

解析後

紫の枠が重なって少しわかりにくいですが、正しくpersonは3人と認識されています。

この人数をDynamoDBにinsertします。
解析が終わった画像は別のS3バケットに移動しておきます。
解析後の出力画像も後の検証のため合わせてS3にアップロードしておきます。

今回このフェーズ2はpythonスクリプトで行っています。
ソースは下記になります。

import boto3
import botocore
import subprocess
import os
import subprocess

BUCKET_NAME = os.environ["BUCKET_NAME"]
BUCKET_NAME_DONE = os.environ["BUCKET_NAME_DONE"]
DYNAMODB_REGION = os.environ["DYNAMODB_REGION"]
DYNAMODB_TABLE = os.environ["DYNAMODB_TABLE"]

s3 = boto3.resource('s3')
bucket = s3.Bucket(BUCKET_NAME)
client = boto3.client('s3')

for obj in bucket.objects.all():
    key = obj.key
    shared_space_name, filename = key.split('/')

    # "prefix/"のobjectも取得してくるのでskip
    if not filename:
      continue

    created_at, extention = filename.split('.')

    # download
    s3.Bucket(BUCKET_NAME).download_file(key, 'iphone.jpg')

    command = "convert iphone.jpg -auto-orient converted.png"
    proc = subprocess.Popen(
      command,
      shell  = True,
      stdin  = subprocess.PIPE,
      stdout = subprocess.PIPE,
      stderr = subprocess.PIPE)

    stdout_data, stderr_data = proc.communicate()

    # yolo
    command = "cd darknet;./darknet detect cfg/yolo.cfg yolo.weights ../converted.png | grep person | wc -l"
    proc = subprocess.Popen(
      command,
      shell  = True,
      stdin  = subprocess.PIPE,
      stdout = subprocess.PIPE,
      stderr = subprocess.PIPE)

    stdout_data, stderr_data = proc.communicate()

    value = int(stdout_data.decode('ascii'))

    # insert dynamo
    dynamodb = boto3.resource('dynamodb', region_name=DYNAMODB_REGION)
    table = dynamodb.Table(DYNAMODB_TABLE)

    resposne = table.put_item(
      Item = {
        'shared_space_name' : shared_space_name,
        'created_at' : created_at,
        'value': value
      }
    )

    # copy file to done bucket
    copy_source = {
      'Bucket': BUCKET_NAME,
      'Key': key
    }
    s3.meta.client.copy(copy_source, BUCKET_NAME_DONE, key)

    # delete file
    obj.delete()

    # upload image
    yolo_key = shared_space_name + '/' + created_at + '_yolo.png'
    client.upload_file('darknet/predictions.png', BUCKET_NAME_DONE, yolo_key)

フェーズ3

DynamoDBのデータを可視化するためre:dashを使います。

今回タイムスタンプはString型でYYYYMMDDhhmmssの形式で入れていましたが
そのまま取得すると正常な時間にならなかったので、
時間の文字列にTIMESTAMP関数をかぶせて、正常な時間を表示するようにしました。

以下が取得に使ったDQLです。

SCAN shared_space_name,TIMESTAMP(created_at),value FROM tbl_name

結果は以下のようなグラフになりました。

なお、re:dashと解析用スクリプトは1台のEC2にdockerを入れてそれぞれコンテナとして動かしています。

課題

閾値

解析した中には1人しか写ってない画像を2人として判別してしまうケースが稀にありました。
darknetは閾値の指定がthreshオプションでできるので、
personである確率が低い場合はカウントしない、などの設定が可能です。
今回はデフォルトの25%で動かしましたが、この値の調整で精度があがるかもしれません。

利用していない人の映り込み

共有スペース近くをたまたま歩いている人が映り込むと人数が実際より多く計測されてしまいます。
そこまで精度を求めないなら無視して構わないかもしれませんが、
1回の計測で数十秒おきに複数枚撮影して、写っている人数の最小値をとるなどすれば、
写り込んだ人の数は排除できるかなと思っています。(こちらもまだ試していません。)

最後に

株式会社ギフティではエンジニアを募集中です。
今回のシステムに興味を持たれた方、新しいオフィスに遊びに来たい方は
お気軽にこちらまでどうぞ