物体検出アルゴリズムSSDをAzure App Service上で動かしてみる

ワンパターンかもしれませんが、[こちらの](https://github.com/ljanyst/ssd-tensorflow)物体検出アプリをWeb化してみました。

###[demo→2](https://tensor-vin.azurewebsites.net)
※タイムアウトした場合もう一度画面更新してみて下さい。(FREE版なので都度起動しているので重たいようです。)
#Jupyter NotebookでサンプルからWeb化の手順
Azure環境は[こちら](https://qiita.com/vye03702/items/7315cc6d83f4d08deffd)。
1.まずJupyterNotebookの内容をFTPでアップします。JupyterNotebookの中で独自のライブラリを定義している場合、フォルダの構造は変えません。ノートブックの.ipynbファイルがあるフォルダにmainプログラムを置きました。
※wwwrootにmainプログラムを置いて、addpathで使用するライブラリの場所を指定で動作を試みましたが、うまくいきませんでした。
2.プログラムの中で、ライブラリを読み込む箇所・関数/オブジェクトの定義箇所は、ブラウザのリクエストから呼び出す関数の外側に配置します。
3. メインプログラム入力をリクエストで受け取り、出力をレスポンスとして返します。これはFlaskならFlask、DjangoならDjangoのお作法に従います。
4.notebooks.visualizationの関数はmainプログラムに持ってきて、ファイル名を与えて、そのファイル名で保存するようにしました。

```python:
wwwroot
├─LogFiles
├web.config
└─SSD-Tensorflow-master
      │
      ├─checkpoints
      │       └ssd_300_vgg.ckpt  学習済のモデル
      │
      ├─datasets
      │     └ライブラリ、今回は使っていない
      ├─deployment
      │     └ライブラリ、今回は使っていない
      ├─net
      │     └ssd_vgg_300.py等のライブラリ      │
      ├─notebooks    jupyterNooteBookの置き場。今回ここにmain.pyを置いてFlaskの起点としている。
      │        ├common.py
      │        ├main.py
      │        ├─static
      │        │      └─result  アップロード画像、生成した画像の置き場
      │        └─templates  htmlの置き場
      │
      ├─preprocessin
      │     └ssd_vgg_preprocessing等のライブラリ
      └─tf_extended
            └ライブラリ、今回は使っていない
```


#プログラムソース
最初の頃、3回ぐらいファイルアップして識別させると、process_image関数のisess.runが実行されずtimeoutしていたのですが、解析のlogをいれていたところ安定して動作するようになったのでログはそのまま残しています。またプログラム理解用に変数やオブジェクトに連番をつけたのもそのままにしています。

#ソース
```html:ssdUp.html
{% extends "base.html" %}
{% block body %}

<div class="container">
<form action="/ssdUp" method="post" enctype="multipart/form-data">
<h2>SSDアップロード</h2>	
<table>
		<tr><td>
            <input type="file" id="uploadFile" name="uploadFile" onchange="$('#file_text').val($(this).val())" style="display:none; " >
            <input type="text" id="file_text" placeholder="file Select please" onClick="$('#uploadFile').click();">
            <input type="submit" value="submit"/>
        </td></tr>

		<tr>
			<td>msg = {{msg}}</td>
        </tr>
	    {% for result_class in result %}
            <tr>
                <td>result = {{ result_class }}</td>
            </tr>
        {% endfor %}
		<tr>
			<td><img src="{{ url_for('static', filename=file_url) }}"></td>
		</tr>
</table>
</form>

</div>
{% endblock %}

```


```python:main.py
# -*- coding: utf-8 -*-
import sys
sys.path.append("SSD-Tensorflow-master/")

from notebooks.common import *  #azure
#from common import *            #gce

log('\n--TOP------------st:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo')))  + ':' + str(os.getcwd()) )

import tensorflow as tf
from tensorflow.python.client import device_lib

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import matplotlib.cm as mpcm

import gc    

#import cv2
import random

import psutil

application = Flask(__name__)
application.config.from_object(__name__)
##################################################################
# ssd  初期定義
##################################################################
log('--ssdDef---------st:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))

UPLOAD_DIR = "SSD-Tensorflow-master/notebooks/static/result/"  #azure
#UPLOAD_DIR = "static/result/"                                  #gce
label_names = ['aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 
              'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 
              'dog', 'horse', 'motorbike', 'person', 'pottedplant',
              'sheep', 'sofa', 'train', 'tvmonitor']

slim = tf.contrib.slim

from nets import ssd_vgg_300
from nets import np_methods
from preprocessing import ssd_vgg_preprocessing
# from notebooks import visualization

# TensorFlow session: grow memory when needed. TF, DO NOT USE ALL MY GPU MEMORY!!!
# gpu_options_1 = tf.GPUOptions(allow_growth=True)
# config_2 = tf.ConfigProto(log_device_placement=False, gpu_options=gpu_options_1)
# isess_3 = tf.InteractiveSession(config=config_2)
isess_3 = tf.InteractiveSession()

# Input placeholder.
net_shape_4 = (300, 300)
data_format_5 = 'NHWC'
img_input_6 = tf.placeholder(tf.uint8, shape=(None, None, 3))

# Evaluation pre-processing: resize to SSD net shape.
image_pre_7, labels_preNouse, bboxes_preNouse, bbox_img_8 = ssd_vgg_preprocessing.preprocess_for_eval(
    img_input_6, None, None, net_shape_4, data_format_5, resize=ssd_vgg_preprocessing.Resize.WARP_RESIZE)
image_4d = tf.expand_dims(image_pre_7, 0)

# Define the SSD model.
reuse_9 = True if 'ssd_net' in locals() else None
ssd_net_22 = ssd_vgg_300.SSDNet()

with slim.arg_scope(ssd_net_22.arg_scope(data_format=data_format_5)):
    predictions_10, localisations_11, _, _ = ssd_net_22.net(image_4d, is_training=False, reuse=reuse_9)

# Restore SSD model. 
ckpt_filename_23 = 'SSD-Tensorflow-master/checkpoints/ssd_300_vgg.ckpt'
# ckpt_filename_23 = '../checkpoints/VGG_VOC0712_SSD_300x300_ft_iter_120000.ckpt'
isess_3.run(tf.global_variables_initializer())


saver = tf.train.Saver()
saver.restore(isess_3, ckpt_filename_23)
# SSD default anchor boxes.
ssd_anchors_13 = ssd_net_22.anchors(net_shape_4)

log('--ssdDef---------ed:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))

##################################################################
# 初期メニュー
##################################################################
@application.route('/')
def index():
    log('--index----------ST:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))
    return render_template('index.html')
    
#各画面からの戻りの時はこちら(/でうまく戻れないので) ※indexとIndexをわざと変えている        
@application.route('/Index',methods=['POST'])
def Index():
    return render_template('index.html')
##################################################################
# python ? Flaskでキャッシュを無効にする  https://codeday.me/jp/qa/20190302/348924.html
##################################################################    
@application.after_request
def add_header(r):
    """
    Add headers to both force latest IE rendering engine or Chrome Frame,
    and also to cache the rendered page for 10 minutes.
    """
    log('--after_request--ST:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))
    r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    r.headers["Pragma"] = "no-cache"
    r.headers["Expires"] = "0"
    r.headers['Cache-Control'] = 'public, max-age=0'
    
    # log('gc.get_stats2 = ' + str((gc.get_stats()[2])))  #ガーベージコレクション、今回ははずした。
    # log('gc = ' + str(gc.collect()))
    # log('gc.get_stats2 = ' + str((gc.get_stats()[2])))

    return r

########################################
# image processing routine.
########################################
def process_image_14(img_15, select_threshold=0.5, nms_threshold=.45, net_shape_4=(300, 300)):
    log('process_image_14-ST:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))) + '-------------')

    # Run SSD network.    
    rimgNouse, rpredictions_16, rlocalisations_21, rbbox_img_17 = isess_3.run([image_4d, predictions_10, localisations_11, bbox_img_8],
                                                            feed_dict={img_input_6: img_15})

    log('process_image_14--01:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))) + '-------------')
    # Get classes and bboxes from the net outputs.
    rclasses_18, rscores_19, rbboxes_20 = np_methods.ssd_bboxes_select(
            rpredictions_16, rlocalisations_21, ssd_anchors_13,
            select_threshold=select_threshold, img_shape=net_shape_4, num_classes=21, decode=True)

    rbboxes_20 = np_methods.bboxes_clip(rbbox_img_17, rbboxes_20)
    rclasses_18, rscores_19, rbboxes_20 = np_methods.bboxes_sort(rclasses_18, rscores_19, rbboxes_20, top_k=400)
    rclasses_18, rscores_19, rbboxes_20 = np_methods.bboxes_nms(rclasses_18, rscores_19, rbboxes_20, nms_threshold=nms_threshold)
    # Resize bboxes to original image shape. Note: useless for Resize.WARP!
    rbboxes_20 = np_methods.bboxes_resize(rbbox_img_17, rbboxes_20)

    log('process_image_14--ED:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))) + '-------------')
    return rclasses_18, rscores_19, rbboxes_20

########################################
# Matplotlib show...notebooks.visualizationの関数
########################################
def plt_bboxes(img_15, classes, scores, bboxes,outFile, figsize=(10,10), linewidth=1.5):
    """Visualize bounding boxes. Largely inspired by SSD-MXNET!
    """
    log('--plt_bboxes-----ST:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))
    fig = plt.figure(figsize=figsize)
    plt.imshow(img_15)
    height = img_15.shape[0]
    width = img_15.shape[1]
    colors = dict()
    for i in range(classes.shape[0]):
        cls_id = int(classes[i])
        if cls_id >= 0:
            score = scores[i]
            if cls_id not in colors:
                colors[cls_id] = (random.random(), random.random(), random.random())
            ymin = int(bboxes[i, 0] * height)
            xmin = int(bboxes[i, 1] * width)
            ymax = int(bboxes[i, 2] * height)
            xmax = int(bboxes[i, 3] * width)
            rect = plt.Rectangle((xmin, ymin), xmax - xmin,
                                ymax - ymin, fill=False,
                                edgecolor=colors[cls_id],
                                linewidth=linewidth)
            plt.gca().add_patch(rect)
            class_name = str(cls_id)
            plt.gca().text(xmin, ymin - 2,
                        '{:s} | {:.3f}'.format(class_name, score),
                        bbox=dict(facecolor=colors[cls_id], alpha=0.5),
                        fontsize=12, color='white')

    log('--plt_bboxes-----ED:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))

    plt.savefig(outFile)

##################################################################
#
#  ssd Main 
#
##################################################################
@application.route('/ssdUp', methods=['POST'])
def ssdUp():

    log('\n--ssdMain------ST:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))

    log('psutil.virtual_memory1 = ' + str(psutil.virtual_memory()))                 #メモリ使用状況
    log('psutil.cpu_percent1 = ' + str(psutil.cpu_percent(interval=1)))             #CPU使用状況
    log('device_lib.list_local_devices = ' + str(device_lib.list_local_devices()))  #GPU確認


    if 'uploadFile' not in request.files:
        return render_template('ssdUp.html', file_url='', msg = 'uploadFile is required', result = '')
        
    file = request.files['uploadFile']
    fileName = file.filename

    if '' == fileName:
        return render_template('ssdUp.html', file_url='', msg = 'uploadFile is required', result = '')

    try:
        saveFile = os.path.join(UPLOAD_DIR, fileName)  
        file.save(saveFile)       
    except:
        return render_template('ssdUp.html', file_url='',   msg = 'save  error', result = '')

    try:
        img_15 = mpimg.imread(saveFile)

        rclasses_18, rscores_19, rbboxes_20 =  process_image_14(img_15)

        outFileName = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_") + fileName
        outFile = os.path.join(UPLOAD_DIR, outFileName)  
        plt_bboxes(img_15, rclasses_18, rscores_19, rbboxes_20,outFile)
    except:
        return render_template('ssdUp.html', file_url='',  msg = 'ssd error', result = '')

    result_class = [] 
    i = 0
    try:                                         #while
        while rclasses_18[i] is not None:
            num = int(rclasses_18[i]) - 1
            result_class.append(label_names[num])
            i = i + 1
    except:
            log('--ssdMainr----ERROR:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))
        
    
    file_url = "result/" + outFileName
    
    log('--ssdMain--------ED:' + str(datetime.datetime.now(pytz.timezone('Asia/Tokyo'))))
    return render_template('ssdUp.html', file_url=file_url, msg = 'ok', result = result_class )
```



```other:その他
main.pyの位置を変更したのでweb.configを下記のようにしています。
    <add key="WSGI_HANDLER" value="SSD-Tensorflow-master.notebooks.main.application" />
```

[index.html](https://qiita.com/vye03702/items/7315cc6d83f4d08deffd)、[web.config、base.html、common.py](https://qiita.com/vye03702/items/b62d845d520a47da50de)はリンク先ページをご覧ください。



入力はjpegファイルを選んでアップすると判別してくれます。
![ssd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228860/71fa1cd0-ed73-6793-a09d-5c81e1fc4fcf.png)


そろそろWeb化は終わりにして、ライブラリの中身の理解に進もうと思っています。