Kerasの学習済みモデルを使ってサクッとWebアプリ作成

画像をアップロードするとそれが何であるかを判別するWebアプリを作ります。

# 作成

## 1. 仮想環境やライブラリインストール諸々

pip と venv を使います。同等の他の方法でも大丈夫です。
pythonは3系です


```shell
python -m venv .
source bin/activate
pip install keras tensorflow numpy pillow bottle
pip freeze > requirements.txt
```

## 2. 画像判別クラス作成

使用するモデルを差し替えやすいように2ファイルにしていますが、まとめてもいいと思います。

keras_app.py

```python
from keras.applications.vgg16 import VGG16 as KerasApp, preprocess_input, decode_predictions

target_size=(224, 224)
```

discriminator.py

```python
from keras_app import KerasApp, preprocess_input, decode_predictions, target_size
import keras.preprocessing.image as PilImage
import numpy

class Discriminator:

  def __init__(self):
    self.model = KerasApp(weights='imagenet')

  def predict(self, file):
    image = PilImage.load_img(file, target_size=target_size)
    x = PilImage.img_to_array(image)
    x = numpy.expand_dims(x, axis=0)
    x = preprocess_input(x)
    preds = self.model.predict(x)
    results = decode_predictions(preds, top=3)[0]
    return list(map(lambda result:{'name': result[1], 'ratio': numpy.float(result[2]) }, results))
```

## 3. サーバ処理作成

[bottle](https://bottlepy.org/docs/dev/)という軽量フレームワークを使います。

画像をアップロードするためのページ(index.html)の表示と画像を受け取って判別結果を返すWebAPIを作ります。

```python
from bottle import route, run, static_file, request, response
from os import getenv, path
import json

from discriminator import Discriminator

def relative_path(target_path):
  return path.normpath(path.join(path.dirname(__file__), target_path))

@route('/')
def index():
  response.content_type = 'text/html; charset=utf-8'
  return static_file('index.html', root = relative_path('./'))

@route('/api/upload', method='POST')
def upload():
  upload = request.files.get('upload')
  result = discriminator.predict(upload.file)
  return json.dumps(result)

discriminator = Discriminator()

run(host='0.0.0.0', port=getenv('PORT', 8080), debug=True)
```

## 4. クライアント処理

カメラボタンをクリックorタップすると保存済みの画像や、カメラを起動して撮った写真をアップロードし、判別結果を表示する処理です。

```html
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>画像判別機</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
  <style type="text/css">
.wrapper {
  padding: 60px 40px;
}
.upload-wrapper {
  position: relative;
  width: 300px;
  height: 300px;
  border: 3px solid black;
  text-align: center;
  vertical-align: middle;
  display:table-cell;
}
#preview {
  width: 300px;
  height: 300px;
  position: absolute;
  top: 0px;
  left: 0px;
}
#result {
  white-space: pre-wrap;
}
  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/native-promise-only/0.8.1/npo.js" integrity="sha256-o/UXdF4sFrbgV5UCIWF5ca7VMLDdplhzA4knJ4nFsc0=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.4/fetch.min.js" integrity="sha256-eOUokb/RjDw7kS+vDwbatNrLN8BIvvEhlLM5yogcDIo=" crossorigin="anonymous"></script>
  <script>
(function () {
  function uploadFile (file) {
    var formData = new FormData();
    formData.append('upload', file)
    return fetch('/api/upload', {
      method: 'POST',
      body: formData
    }).then(function (response) {
      return response.json();
    });
  }
  function showResult (text) {
    var result = document.getElementById('result');
    result.innerHTML = text;
  }
  document.addEventListener('DOMContentLoaded', function () {
    showResult('画像を選択して下さい');
    var upload = document.getElementById('upload');
    var preview = document.getElementById('preview');
    upload.addEventListener('change', function (e) {
      preview.removeAttribute('src');
      var file = e.target.files[0];
      if (!file) {
        showResult('画像を選択して下さい');
        return;
      }
      showResult('判別中...');
      var uploadFileReader = new FileReader();
      uploadFileReader.readAsDataURL(file);
      uploadFileReader.onload = function () {
        preview.setAttribute('src', uploadFileReader.result);
      };
      uploadFile(file).then(function (results) {
        showResult(results.map(function (result) {
          return result.name + ': ' + (Math.round(result.ratio * 10000) / 100) + '%';
        }).join('\n'));
      }).catch(function (e) {
        showResult('判別に失敗しました。');
        console.error(e);
      });
    });
  });
})();
  </script>
</head>
<body>
  <div class="wrapper">
    <div class="upload-wrapper">
      <label for="upload">
        <i id="icon-camera" class="fa fa-camera fa-5x" aria-hidden="true"></i>
        <img id="preview" />
      </label>
      <input id="upload" type="file" name="upload" accept="image/*" style="display:none;" />
    </div>
    <div id="result">画像が選択されていません</div>
  </div>
</body>
</html>
```

# 実行

## 起動

```
python server.py
```

初回はモデルのダウンロードが走るので起動が遅いです。

http://localhost:8080 をWebブラウザで開くと表示されます。

同一LAN内の別PCやスマートフォン・タブレットで表示したい場合は `http://(起動マシンのIP):8080` で開けます。

## 使い方

カメラアイコン部分をタップするとファイルダイアログが表示されます。画像を選択するとサーバへアップロードされ判別結果が返ってきます。

![howto.gif](https://qiita-image-store.s3.amazonaws.com/0/21396/4c00ebfe-b499-8874-71f8-80fd191cb005.gif)

# 参考リンク

- [全ソース](https://github.com/boushi-bird/image-discriminator)
- [KerasでVGG16を使う - 人工知能に関する断創録](http://aidiary.hatenablog.com/entry/20170104/1483535144)
- [Applications - Keras Documentation](https://keras.io/ja/applications/)