OpenPoseをブラウザで動かしてみた

diffeasyアドベントカレンダー22日目！

皆さん、[OpenPose](https://github.com/ZheC/Realtime_Multi-Person_Pose_Estimation)って知ってますか？
流行り（？）のDeep LearningとマシンのGPUをゴリゴリ使って、人の姿勢を可視化できるライブラリなんですが、結構精度が良くて、マシンのスペックによってはかなりの人数分の姿勢を可視化できます。こんな感じで。
※gif画像はhttps://github.com/ZheC/Realtime_Multi-Person_Pose_Estimation にある画像です
![image.png](https://raw.githubusercontent.com/ZheC/Realtime_Multi-Person_Pose_Estimation/master/readme/shake.gif)

しかも、他の開発者がCPUでも動く版を作ってて、13インチMBPしか持ってない僕でも、ラクラク試してみることができました😎

[tf-pose-estimation](https://github.com/ildoonet/tf-pose-estimation)

さらに、このCPU版にはWebカメラでリアルタイム検出をするモードがありまして、以下のコマンドを入力すれば起動します！

```bash
$ python3 run_webcam.py --model=mobilenet_thin --resize=432x368 --camera=0
```

で、少し前に卒研ネタでこれを使った姿勢矯正システムを作ったのですが、いくつか問題点が上がってきてました。

具体的には

- コマンド長すぎィ！
- GUIで操作したい
- 統計とか取ってるので、それを見れる画面もほしい
- Python使ってゴリゴリは嫌だ

って感じだったので、ブラウザで操作できたら最高やんと思い、作ってみました。

# 開発環境
マシン：MBP 13inch
Python version：3.5.0
pip version：18.1

# tf-pose-estimatorのセットアップ
基本的には、GithubのReadme通りに実行すると動くのですが、`pip3 install -r requirements.txt`を流すと`Command "python setup.py egg_info" failed with error code 1 in `とエラーが出てくると思うので、requirement.txtを流す前に以下のようにします。

```bash
$ pip3 install --upgrade pip3 setuptools
$ pip3 install ez-setup 
$ git clone https://github.com/Zulko/unroll
$ pip3 install ./unroll
```

※以下のstach overflowの回答を参考にしています
[pip でインストールエラー: Command “python setup.py egg_info” failed](https://ja.stackoverflow.com/questions/46978/pip-%E3%81%A7%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%82%A8%E3%83%A9%E3%83%BC-command-python-setup-py-egg-info-failed?rq=1)

また、動かそうとするとOpenCVとTensorFlowを入れてない場合動かないので、それらもインストールします。

```bash
$ pip3 install tensorflow opencv-python
```

セットアップ後、tf-openposeの直下で以下のコマンドを入力すれば、リアルタイムでキャプチャしている画面が出力されます✨

```bash
$ python3 run_webcam.py --model=mobilenet_thin --resize=432x368 --camera=0
```

**done**👍

# Flaskサーバ立てて、jsでリアルタイムキャプチャ
FlaskやJinja2の情報は結構出回ってるのですが、Web上でのリアルタイムキャプチャはあんまり情報なくて悪戦苦闘しました。

## APIサイド
```python:tf-openpose/server.py
# -*- coding: utf-8 -*-
from flask import Flask, request, render_template, Response
import run_web
import os
from datetime import datetime as dt

app = Flask(__name__)

# template route
@app.route('/')
def index():
  return render_template('index.html')

# api route
@app.route('/api/estimator', methods=["POST"])
def estimator():
  # imageのリクエストを受けて保存
  image = request.files["img"]
  file_name = dt.now().strftime('%Y%m%d%H%M%S') + '.png'
  image.save(os.path.join('./img/', file_name))
  # OpenPoseに投げる
  image = run_web.run(file_name)
  # OpenPose側で出力された画像を返す
  f = open('./img/' + file_name, 'rb')
  image = f.read()
  os.remove(os.path.join('./img/', file_name))
  return Response(response=image, content_type='image/png')

if __name__ == "__main__":
  app.run("127.0.0.1", debug=True)
```
```python:tf-oepnpose/run_web.py
import argparse
import logging
import time

import cv2
import numpy as np

from tf_pose import common
from tf_pose.estimator import TfPoseEstimator
from tf_pose.networks import get_graph_path, model_wh

logger = logging.getLogger('TfPoseEstimator-WebCam')
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

fps_time = 0

# run.pyをAPI用に必要な部分だけ切り取ったもの
def run(image_name):
    w, h = model_wh('1280x720')
    e = TfPoseEstimator(get_graph_path('mobilenet_thin'), target_size=(w, h))

    logger.debug('image process+')
    image = common.read_imgfile('./img/' + image_name, None, None)
    if image is None:
        logger.error('Image can not be read, path=%s' % image_file)
        sys.exit(-1)
    humans = e.inference(image, resize_to_default=(w > 0 and h > 0), upsample_size=4.0)

    logger.debug('postprocess+')
    # ここから姿勢のポイントを画像に書き込み、点と線が追加された画像が返ってくる
    image = TfPoseEstimator.draw_humans(image, humans, imgcopy=False)

    cv2.imwrite('./img/' + image_name, image)
```

## フロントサイド
```html:tf-openpose/templates/index.html
{% extends "layout.html" %}
{% block body %}

<video id="player" autoplay></video>
<canvas width="1280" height="720" id="res-image"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.17.1/axios.min.js"></script>
<script>
var player = document.getElementById('player'); 
var resImage = document.getElementById('res-image');

// Webカメラの起動に少し時間がかかったので、3.5s待たせる
function wait() {
    console.log('Starting...');
    return new Promise(resolve => setTimeout(resolve, 3500));
};

var handleSuccess = function(stream) {
    player.srcObject = stream;
    wait()
        .then(function() {
            drawCanvas();
            captureCanvas();
        })
}


function captureCanvas () {
    console.log('capture!')
    // canvasのtoBlobを使い、キャプチャしたデータをblob化
    snapshotCanvas.toBlob(function(blob) {
        var formData = new FormData();
        params = new FormData();
        params.append('img', blob);
        // axiosを使ってAPIへPOST
        axios.post('http://localhost:5000/api/estimator', params, { responseType: 'blob', 'headers': { 'Content-Type': 'image/png' } })
            .then(function(response) {
                drawResponseSnapCanvas(response.data);

                // 再帰呼び出し
                drawCanvas();
                captureCanvas();
            });
    });
}

function drawResponseSnapCanvas (image) {
    var resImageContext = resImage.getContext('2d');
    var img = new Image();
    var reader = new FileReader();
    var dataUrl;
    // FileReaderを使ってcanvasへ出力
    reader.readAsDataURL(image);
    reader.onloadend = function () {
        dataUrl = reader.result;
        img.onload = function () {
            resImageContext.drawImage(img, 0, 0, resImage.width, resImage.height);
        }
        img.src = dataUrl;
    }
}

// videoタグを使ってWebカメラ起動し、画像取得
navigator.mediaDevices.getUserMedia({video: { width: 1280, height: 720 }})
    .then(handleSuccess);

</script>

{% endblock %}
```

ざっとコードを丸張り丸投げしましたが、こんな感じです。
ブラウザ側では常にキャプチャし続け、API側で姿勢点の作成を行う、といった感じです。
最後に

```bash
$ python3 server.py
```
でWebサーバ起動して、`localhost:5000`にアクセスすればOKです。起動コマンド短くなって、嬉しいですね。

# OpenPose Webの課題点
OpenPoseがWebで起動できるようになり、入力が短くなったことは大変嬉しいことですが、現状まだまだ課題点がありました。

- APIコールの度にモデル全読みしてて、一回コールすると、5秒間ほどなにも起こらない
- run_webcam.py使ったときよりもめっちゃファン回る → Mac死ぬんじゃない…？

などです。
僕はまだ、Pythonは楽しむ程度にしか戯れたことがないので、ちょっとこの辺り詳しい人に教えてもらいたいなーと思いつつ、この記事を書きました。

ただ、1枚の画像から人の姿勢状態を取れると、姿勢矯正システム作りとかめっちゃ捗るなと思っているので、今後のIT業界へ「猫背🐈」を矯正できるシステムが、流星の如く現れてくれることを、楽しみにしています😸