26
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue.js(Vuetify) + Flask で超解像を試せるアプリを作った(Herokuにデプロイまで)

Last updated at Posted at 2021-10-21

※10/26 更新
画像アップロードの際にドラッグ&ドロップじゃなく、クリックしてファイル選択するやり方だと正しくアップロードされないようです。まだまだVue.jsの勉強が必要ですね。

はじめに

最近はひたすら超解像コンペに取り組んでいますが、まだ超解像という分野がマイナーな印象なので、知らない方にもどういうものか試せるようにと思い、フロント側はVue.js、バックエンドはPython(Flask)でWebアプリケーション化してHerokuにデプロイするまでの記録です。

Vue.jsとFlaskの連携方法に関しては、先人の素晴らしい記事がありますが、個人的につまる部分もあったので同じように連携させたい方の参考になれば幸いです。

また自分がメインで使用している言語はPythonで、プログラミングを勉強し始めてもうすぐ4ヶ月の人間です。HTML,CSSは軽くかじった程度で、Javascriptの知識はほぼゼロ状態からのスタートです。デザインセンスは壊滅的だし、ひたすらググりながら「理解」よりも「動く」を重視して作成したので、間違い等が散見されると思います。遠慮なくご指摘いただければ幸いです。

アプリへのリンクはコチラ→超解像アプリ

現状画像サイズが大きすぎるとメモリオーバーで動作が止まるので、もし動かす場合は以下の画像たち
chip.png 0030.jpg ADE_val_00000114.jpg

で「そのまま超解像」をオンにして試して頂けると幸いです。
またSwinIR(big)に関してはモデルデータをherokuサーバーにアップロードしてないのでエラーになるのと、アンサンブルも現状メモリオーバーになってしまうのでエラーになってしまいます。ご了承ください。(エラーハンドリングがうまく効いてるかチェックするためでもある)

アプリ概要

画像をアップロードすると、学習済みモデルに画像を投げて、超解像化した画像を返すというシンプルなものになっています。

主な機能として
・画像アップロード機能
・アラート表示機能
・連続処理中断のため、ボタンの稼働コントロール機能
・モーダルでの使い方表示
・アンサンブルでの超解像(アンサンブルに関してはコチラ)をするか選択できる
・与えられた画像をそのまま高画質化するか、低解像度画像を作るか選択できる

詳細は後述します。

トップページ

スクリーンショット 2021-10-21 13.42.30.png
画像がアップロードされないと「超解像スタート」のボタンが押せないようにしています。

画像をアップロードした時の画面

スクリーンショット 2021-10-21 13.54.15.png
画像がアップロードされると、プレビュー画面が表示され、「超解像スタート」ボタンがハイライトされ、押せるようになります。

超解像終了後の画面

スクリーンショット 2021-10-21 13.54.46.png
超解像処理が終了すると、低解像度画像と超解像画像がプレビューされ、成功したというアラートが表示されます。

デモプレイ

test.gif

開発環境,使用技術

ツール

・M1 Mac OS ver12.0(Monterey beta版)
・VScode
・Google Colabratory(Pro+)

フロント側

・Vue.js: 2.6.12
・Vuetify: 2.4.0
・node: 14.15.1
・npm: 6.14.8
・yarn: 1.22.10
・core-js: 3.18.3

バックエンド側(condaの仮想環境を使用)

・python: 3.8.12
・pytorch: 1.9.0
・Flask: 1.1.2
・Werkzeug: 1.0.1

開発までの流れ

  1. モデルの選定、学習
  2. アプリ雛形作成
  3. フロント側作成
  4. バックエンド側作成
  5. デプロイ

端的にまとめると上記のような形になりますので、順を追って詳しく説明していきます。

1. モデルの選定、学習

超解像タスクにおいて、自分が実際に試してみた超解像モデルは
・SRCNN
・EDSR
・WCAN
・EDN
・WDSR
・FSRCNN
・ESRGAN
・SwinIR
ですが(細かいモデルの詳細は省略)、(個人的に)軽量かつ精度が高いと思ったものがEDSRとWDSRとSwinIRの3つです。

実装・学習するにあたって、EDSRとWDSRモデルは、Tensorflow、SwinIRはPytorchをフレームワークとして使用しました。学習はGoogle Colab Pro+にて行いました。

最終的にHerokuの無料枠(Slug size max 500 MiB)にデプロイするとなった時に、TensorflowPytorchを両方とも入れると余裕でオーバーしてしまったので、SwinIRの軽量版のみHeroku
環境では動くようにしています。

学習の詳細はコチラの記事に従って学習させてますが、コンペ解法のネタバレになりそうなので終了後にでも公開しようかと思います。

あくまでも画像データを扱ったFlaskとVue.jsの連携方法をメインに伝えられればと思います。

もし超解像タスクに興味を持った方がいらっしゃれば、EDSRモデルやWDSRモデルが軽量かつ精度も高く、実装も簡単なのでオススメです。

2. アプリ雛形作成

当初のディレクトリ構造は以下です。

SR_app
 |-- front/ 
 |-- back/
 `-- README.md

frontディレクトリ以下にvue.js関連のファイルをまとめ、backディレクトリ以下にflask関連のファイルをまとめていました。

ただ最終的にHerokuにデプロイする際に、この構造だとpackage.jsonファイルの読み取りができず、frontフォルダの中身をルートディレクトリに展開するハメになったので、Vueプロジェクトを作成して、その中にbackディレクトリを作成していく方がいいと思います。

今回はvue-cliで作成しました。
2.x系のデフォルトで作成したのでTypescriptは使ってません。

$ vue create front
$ cd front
$ npm run serve    (yarn serveでもok)

フロント側のサーバーがポート8080に立ち上がります。

次にbackディレクトリの構成は最終的には以下のようになります。

back/
|-- No_TTA_make_SR.py 
|-- TTA_make_SR.py
|-- __pycache__
|-- model_set.py
|-- models/
|-- server.py
|-- temporary_images/
`-- utils.py

結論から言うと理想の構成は以下のようになると思います。

root/
|-- back/ (flask関連のファイルをまとめておく)
|
|(root直下にvue関連のファイル、フォルダ一覧がくる 
|  ように)
|

雛形、フォルダ構成を作っておいて次はフロント側を作っていきます。

3. フロント側作成

見た目の部分なのでいじってて楽しかったです。

VueのフレームワークのVuetifyを以下のコマンドで入れます。プリセットはデフォルトです。

$ vue add vuetify

vue.config.jsもいじっておきます。

module.exports = {
  transpileDependencies: [
    'vuetify'
  ],
  assetsDir: 'static',
  configureWebpack: {
    performance: {
      maxAssetSize: 270000000,
      maxEntrypointSize: 700000000,
    }
  }
}

configureWebpackはあまりいじらない方がいいかも。

App.vueの編集

最終的なコードを載せます。

App.vue
<template>
  <v-app>
    <v-app-bar
      app
      color="#0095d9"
      dark>
      <v-toolbar-title>超解像(4倍スケーリング)</v-toolbar-title>

      <v-dialog v-model="dialog" max-width='700'>
        <template v-slot:activator="{ on }">
          <v-btn v-on="on" text>
            使い方
          </v-btn>
        </template>
        <v-card>
          <v-toolbar color='#0095d9' dark>
            使い方
          </v-toolbar>
          <v-card-text>
            <p></p>
            <p> 1. 超解像したい画像をアップロードします。</p>
            <p>2. 「アンサンブル」にチェックを入れると「Self Ensemble」(精度がより上がる)で超解像化します。(時間がかかります。)</p>
            <p>3. 「そのまま超解像」にチェックを入れるとbicubicでの低解像度画像を作成しません。(アップロードする画像が低解像度の場合に有効です。)</p>
            <p>4. モデルは、「SwinIR(big)」「SwinIR(light)」の2種類を選択できます。</p>
            <p>5. 「超解像スタート」を押すと超解像化を行います。</p>
            <p>6. 終わった後は「もう一度超解像」を押すことで変更を変えて試せます。</p>
          </v-card-text>

          <v-divider></v-divider>

          <v-card-actions>
          <v-spacer></v-spacer>
            <v-btn
              color="#d90028"
              text
              @click="dialog = false"
            >
            閉じる
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>

    </v-app-bar>
    <v-main>
      <HelloWorld/>
    </v-main>
  </v-app>
</template>

<script>
import HelloWorld from './components/HelloWorld';
export default {
  name: 'App',

  components: {
    HelloWorld,
  },

  data: () => ({
    dialog: false
  })
}
</script>

今回は1画面のアプリなので、このApp.vuecomponents/HelloWorld.vueをメインにいじります。

上記の中で大枠となるとこだけ抜き出すと以下の感じ。

<template>
  <v-app>
    <v-app-bar>
    </v-app-bar>
    <v-main>
      <HelloWorld/>
    </v-main>
  </v-app>
</template>

この部分はHTML部分を示しています。

<v-app-bar>がアプリケーションバーを表示しており<v-main>の中に<HelloWorld/>が入っています。これは、メイン部分の描画内容をHelloWorld.vueというコンポーネントにするということです。

Vue.jsでは描画内容をファイルを分けて書けるのとHTML部分、CSS部分、Javascript部分を別々に書くのでわかりやすいと思います。

そして

<script>
import HelloWorld from './components/HelloWorld';
export default {
  name: 'App',

  components: {
    HelloWorld,
  },

  data: () => ({
    dialog: false
  })
}
</script>

scriptタグの中でjavascriptのコードを書いています。
import HelloWorld from './components/HelloWorld'でコンポーネントを読み込んでいて、export以下に記載することでapp.vueでも使えるようにしています。

細かい文法は省きますが、コード自体はVuetifyの公式リファレンスに載っているので扱いやすかったです。コピペして貼り付けで任意のコンポーネントが使えるのは手軽でいいですね。

App.vueでは
アプリケーションバー、使い方を表示するモーダルウィンドウを実装しています。

アプリケーションバーの使い方を押すと、こんな感じでモーダルウィンドウで使い方が表示されます。
スクリーンショット 2021-10-21 17.01.43.png

HelloWorld.vueの編集

こちらもとりあえず最終的なコードを載せます。

HelloWorld.vue
<template>
  <v-container class='px-0'>
    <v-alert
      v-model='success'
      dismissible
      elevation="6"
      type="success"
      color='#00d945'
    >超解像に成功しました!</v-alert>
    <v-alert
      v-model='stop'
      dismissible
      elevation="6"
      type="error"
    >エラーが発生しました。</v-alert>
    <v-row justify='center'>
      <v-col md = '9'
        @dragover.prevent
        @dragenter="onDragEnter"
        @dragleave="onDragLeave"
        @drop="onDrop"
      >
        <v-file-input
          v-model="files"
          color="#0095d9"
          label="画像をアップロード"
          prepend-icon="mdi-image"
          outlined
          :show-size="1000"
          :background-color="isDragging ? '#d9b100' : 'null'"
          @change="Imagedelete"
        >
          <template v-slot:selection="{ index, text }">
            <v-chip v-if="index < 2" color="#d9b100" dark label small>{{ text }}</v-chip>
            <span
              v-else-if="index === 2"
              class="overline grey--text text--darken-3 mx-2"
            >+{{ files.length - 2 }} File(s)</span>
          </template>
        </v-file-input>
      </v-col>
    </v-row>
    <v-row justify='center'>
      <v-col md='3'>
        <v-checkbox
          v-model="checkbox"
          :label="`アンサンブル`"
          color="#0095d9"
        ></v-checkbox>
      </v-col>
      <v-col md='3'>
        <v-checkbox
          v-model="checkbox2"
          :label="`そのまま超解像`"
          color="#0095d9"
        ></v-checkbox>
      </v-col>
      <v-col md='3'>
        <v-select
          :items="models"
          label="モデルを選択"
          v-model='selectedModel'
          item-text='label'
          item-value='value'
          color='#0095d9'
          return-object
        ></v-select>
      </v-col>
    </v-row>
    <v-row justify='center'>
      <v-btn
        :disabled="!isfile"
        color="#d90028"
        class="ma-2 white--text"
        @click="uploadImage"
      >{{btn_name}}
      </v-btn>
    </v-row>
    <v-row>
      <v-col md='4'>
        <v-card v-if="uploadImageUrl">
          <v-card-title class='justify-center'>アップロードされた画像
            <img id='uploadimg' :src= 'uploadImageUrl' width='100%' height='auto'>
            <img id='uploadimg2' :src= 'uploadImageUrl' style='display: none'>
          </v-card-title>
        </v-card>
      </v-col>
      <v-col md = '4' class='ml-auto'>
        <v-card v-if='LRUrl'>
          <v-card-title class='justify-center'>低解像度画像
            <img :src= 'LRUrl' width='100%' height="auto">
          </v-card-title>
        </v-card>
      </v-col>
      <v-col md = '4' class='ml-auto'>
        <v-card v-if='SRUrl'>
          <v-card-title class='justify-center'>超解像画像
            <img :src= 'SRUrl' width='100%' height="auto">
          </v-card-title>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import axios from 'axios'

export default {
  name: "HelloWorld",

  data: () => ({
    files: [],
    isfile: false,
    isDragging: false,
    dragCount: 0,
    success: false,
    stop: false,
    uploadImageUrl: '',
    img_data: '',
    checkbox: false,
    checkbox2: false,
    btn_name: '超解像スタート',
    LRUrl: '',
    SRUrl: '',
    selectedModel: {label: 'SwinIR(big)', modelId: 1},
    models: [
      {label: 'SwinIR(big)', modelId: 1},
      {label: 'SwinIR(light)' , modelId: 2}]
  }),

  methods: {
    onDrop(e) {
      this.stop = false
      e.preventDefault();
      e.stopPropagation();
      this.isDragging = false;
      const _files = e.dataTransfer.files;
      for (const file in _files) {
        if (!isNaN(file)) {

          this.files.push(_files[file])
          this.uploadImageUrl = URL.createObjectURL(_files[file])
          this.isfile = true
        }
      }
    },

    onDragEnter(e) {
      e.preventDefault();
      this.isDragging = true;
      this.dragCount++;
    },

    onDragLeave(e) {
      e.preventDefault();
      this.dragCount--;
      if (this.dragCount <= 0) {
        this.isDragging = false;
      }
    },

    Imagedelete() {
      this.uploadImageUrl = ''
      this.img_data = ''
      this.isfile = false
      this.files = []
      this.LRUrl = ''
      this.SRUrl = ''
      this.btn_name = '超解像スタート'
      this.success = false
    },

    uploadImage() {
      this.isfile = false
      this.btn_name = '実行中...'
      this.LRUrl = ''
      this.SRUrl = ''
      this.success = false
      this.stop = false

      const img = document.getElementById('uploadimg2')
      const cvs = document.createElement('canvas')
      const context = cvs.getContext('2d')
      cvs.width = img.width
      cvs.height = img.height
      context.drawImage(img, 0,0)
      this.img_data = cvs.toDataURL('image/png')

      const params = new FormData()
      params.append('TTA', this.checkbox)
      params.append('low_off', this.checkbox2)
      params.append('image', this.img_data)
      params.append('model_id', this.selectedModel.modelId)

      axios.post('/resolution', params).then(response => {
        const status = response.data.status
        if (status == 'error') {
          this.stop = true
          this.Imagedelete()
        } else {
          this.success = true
          this.LRUrl = response.data.low_img_url
            if (this.checkbox2 == true) {
              this.LRUrl = this.uploadImageUrl
            }
            this.SRUrl = response.data.sr_img_url
            this.btn_name = 'もう一度超解像'
            this.isfile = true
        }
      })
    },
  }
}
</script>

こちらでは上から
・エラー時、成功時のアラート
・画像のアップロード部分
・各種設定項目のチェックボックス
・モデル選択のセレクトボックス
・超解像を始めるスタートボタン
・アップロードされた画像、低解像度画像、高解像度画像のプレビュー部分

を実装しています。
これもコード自体は公式リファレンスに載っているので助かります。

重要なのはscript部分ですかね。

まず<v-input-file>タグで画像のアップロードフォームを実装できますが、デフォルトだとドラッグ&ドロップでのアップロードに対応していません。

そのためコチラを参考にドラッグ&ドロップでのアップロード機能とプレビュー機能を実装しています。

該当部分のコードは以下。

HTML部分
<v-col md = '9'
  @dragover.prevent
  @dragenter="onDragEnter"
  @dragleave="onDragLeave"
  @drop="onDrop"
  >
  <v-file-input
    v-model="files"
    color="#0095d9"
    label="画像をアップロード"
    prepend-icon="mdi-image"
    outlined
    :show-size="1000"
    :background-color="isDragging ? '#d9b100' : 'null'"
    @change="Imagedelete"
    >
     <template v-slot:selection="{ index, text }">
      <v-chip v-if="index < 2" color="#d9b100" dark label small>{{ text }}</v-chip>
        <span
          v-else-if="index === 2"
          class="overline grey--text text--darken-3 mx-2"
         >+{{ files.length - 2 }} File(s)</span>
      </template>
    </v-file-input>
</v-col>
script部分
<script>
....
methods: {
    onDrop(e) {
      this.stop = false
      e.preventDefault();
      e.stopPropagation();
      this.isDragging = false;
      const _files = e.dataTransfer.files;
      for (const file in _files) {
        if (!isNaN(file)) {

          this.files.push(_files[file])
          this.uploadImageUrl = URL.createObjectURL(_files[file])
          this.isfile = true
        }
      }
    },

    onDragEnter(e) {
      e.preventDefault();
      this.isDragging = true;
      this.dragCount++;
    },

    onDragLeave(e) {
      e.preventDefault();
      this.dragCount--;
      if (this.dragCount <= 0) {
        this.isDragging = false;
      }
    },

    Imagedelete() {
      this.uploadImageUrl = ''
      this.img_data = ''
      this.isfile = false
      this.files = []
      this.LRUrl = ''
      this.SRUrl = ''
      this.btn_name = '超解像スタート'
      this.success = false
    },

<v-file-input>タグ全体をdivタグやv-colタグで囲んでその中でdrag関係の機能を定義しないと動かないです。

<v-file-input>タグの中の@changeImagedeleteメソッドを記載することで、画像を消したときにImagedeleteメソッドが作動し、いろいろな変数が初期化するようにしています。

画像をアップロードして、読み込んでプレビュー画面を表示するまでの流れを説明すると

  1. 画像がアップロードされるとonDropメソッドが作動
  2. filesリストに読み取ったファイルを格納
  3. this.uploadImageUrl = URL.createObjectURL(_files[file])でuploadImageUrlという変数に、アップロードされた画像データをbrob URL化したURLを代入
  4. uploadImageURLの値が空でなくなったので、アップロードした画像とタイトルが書かれたv-cardが表示される
  5. this.isfile = trueとすることでファイルが存在しているので「超解像スタート」のボタンの:disabledが無効になり、押せるようになる。

変数やメソッド名を参考に追っていただければどういう動作をしているのかは理解していただけるかと思います。


さて次に読み取った画像をflask側に送る操作についてです。
「超解像スタート」のボタンを押したら、データを送る動作が作動します。

flask側に送りたい情報は

  1. アップロードされた画像データ
  2. アンサンブルを行うのかどうか
  3. アップロードされた画像データを超解像するのか、アップロードされた画像から低解像度の画像を作って、それを超解像するのかどうか
  4. 超解像を行うモデルはどれなのか

の4つです。

調べたところ、1のアップロードされた画像データはVue.js側でbase64形式にして送ることができそうです。

該当コードは以下。

<script>
...

const img = document.getElementById('uploadimg2')
const cvs = document.createElement('canvas')
const context = cvs.getContext('2d')
cvs.width = img.width
cvs.height = img.height
context.drawImage(img, 0,0)
this.img_data = cvs.toDataURL('image/png')

...
</script>

iduploadimg2の要素を取得してキャンバスに描き、toDataURLでbase64形式に変換しています。

また細かいポイントですが、該当部分のHTML部分を見ると

<v-card v-if="uploadImageUrl">
  <v-card-title class='justify-center'>アップロードされた画像
    <img id='uploadimg' :src= 'uploadImageUrl' width='100%' height='auto'>
    <img id='uploadimg2' :src= 'uploadImageUrl' style='display: none'>
  </v-card-title>
</v-card>

imgタグが2つあり、片方はdisplay: noneで非表示になっています。
imgサイズを指定しないとレイアウトが崩れてしまうのでwidthはcol幅マックス、高さはautoにして崩れないようにしています。

ここでid=uploadimg(表示されている画像)の方の画像データをthis.img_dataに代入してしまうと、flask側に送られる画像はサイズが調整されたものになってしまいます。

表示される画像はサイズを調整したものがいいけど、flask側に送る画像は元の画像サイズで送りたいというワガママに対応するために独自で考えました。
多分もっといい方法があるはずなので、教えていただけると幸いです。

これで1の画像データを送る部分は解決しました。


次に2と3に関してですが、これはチェックボックスなのでtrue,false値で送ることができます。

onにすればcheckboxの値がtrue, offにすればcheckboxの値がfalseになるので、これをそのままflask側に送ります。

最後に4ですが、セレクトボックスの値の取得に戸惑ったのでコチラを参考に実装しました。

HTML部分
<v-select
  :items="models"
  label="モデルを選択"
  v-model='selectedModel'
  item-text='label'
  item-value='value'
  color='#0095d9'
  return-object
></v-select>
script部分
<script>
...

selectedModel: {label: 'SwinIR(big)', modelId: 1},
    models: [
      {label: 'SwinIR(big)', modelId: 1},
      {label: 'SwinIR(light)' , modelId: 2}]

...
</script>

表示されるラベルと値を分けるのがポイントです。

flask側に渡す時には1とか2とかの数値で渡すことになります。


上記4つの値を渡すには

const params = new FormData()
params.append('TTA', this.checkbox)
params.append('low_off', this.checkbox2)
params.append('image', this.img_data)
params.append('model_id', this.selectedModel.modelId)

paramsという名前でFormDataオブジェクトを作成し、その中に辞書形式でappendメソッドで追加していきます。
この辺りはPythonのリストに追加していくのと似てて、わかりやすいです。

そして、値が格納されたparamsaxiosを使ってflask側に送ります。

axios.post('/resolution', params).then(response => {
   const status = response.data.status
        if (status == 'error') {
          this.stop = true
          this.Imagedelete()
        } else {
          this.success = true
          this.LRUrl = response.data.low_img_url
            if (this.checkbox2 == true) {
              this.LRUrl = this.uploadImageUrl
            }
            this.SRUrl = response.data.sr_img_url
            this.btn_name = 'もう一度超解像'
            this.isfile = true
        }
      })

/resolutionparamsを転送して、何かしらのエラーが出たらそこで処理を中断し、そうでなければflask側で生成された低解像度画像、超解像画像を受け取るようになっています。

これでフロント側の見た目と、データを送る部分の実装は以上になります。
次に受け取ったデータを処理するコードをpythonで書いていきます。

4. バックエンド側作成

もう一度バックエンド側のディレクトリ構造を示します。

backのディレクトリ構造
back
|-- No_TTA_make_SR.py
|-- TTA_make_SR.py
|-- __pycache__
|-- model_set.py
|-- models/
|-- server.py
|-- temporary_images/
`-- utils.py

各ファイル、ディレクトリの詳細は以下。

・No_TTA_make_SR.py
アンサンブルをしないで超解像処理を行う

・TTA_make_SR.py
アンサンブルをして超解像処理を行う

・model_set.py
選択されたモデルに応じてモデルを構築する

・models/
学習済みモデルデータ、モデル定義のpythonファイルが格納されている

・server.py
サーバー、アプリの立ち上げ、各種ルーティング処理、基準になるファイル

・temporary_images/
生成した低解像度画像、超解像画像を保存しておくディレクトリ

・utils.py
各種関数の定義

正直いろんな関数は全部utils.pyにまとめられるので、もう少し綺麗にしたいですね。

ともあれ、サーバーやアプリの立ち上げ処理を記載するserver.pyを軸に説明していきます。


まずコード全体を記載します。
ルーティング部分の実装はコチラを参考にしています。

server.py
import base64
import os
from flask import Flask, render_template, jsonify, request, redirect, make_response
from flask.helpers import send_from_directory

from utils import make_LR_img, PIL_to_base64
from model_set import define_model
from No_TTA_make_SR import make_SR_img_TTA_off
from TTA_make_SR import make_SR_img_TTA_on

app = Flask(__name__, static_folder='../dist/static', template_folder='../dist')

app.config['JSON_AS_ASCII'] = False

IMAGE_FOLDER = './back/temporary_images/'

@app.route('/', defaults={'path':''})
@app.route('/<path:path>')
def index(path):
    return render_template('index.html')

@app.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(app.root_path, 'temporary_images') ,'favicon.ico')

@app.errorhandler(500)
def something_error(error):
    return jsonify({'status': 'error'})

@app.route('/resolution', methods=['GET', 'POST'])
def uploads_file():
    if request.method == 'POST':
        print('requestを受け取りました。')

        isTTA = request.form['TTA']
        no_make_low = request.form['low_off']
        model_id = request.form['model_id']
        upload_image_file = request.form['image']

        LR_img = make_LR_img(upload_image_file, low_off=no_make_low)
        model = define_model(model_id)

        if isTTA == 'true':
            print('超解像スタート')
            make_SR_img_TTA_on(model, LR_img)
        else:
            print('超解像スタート')
            make_SR_img_TTA_off(model, LR_img)
        print('超解像終了')

        if no_make_low == 'false':
            base64_low_img = PIL_to_base64(IMAGE_FOLDER + 'low_img.png')
        else:
            base64_low_img = 'aaa'

        base64_sr_img = PIL_to_base64(IMAGE_FOLDER + 'sr_img.png')

        return jsonify({'low_img_url': 'data:image/png;base64,'+ base64_low_img,
                        'sr_img_url': 'data:image/png;base64,'+ base64_sr_img})

if __name__ == '__main__':
    #app.run(host='0.0.0.0', port=5010, debug=True)
    app.run()

重要なところを説明すると

app = Flask(__name__, static_folder='../dist/static', template_folder='../dist')

appの定義部分ですが、static_foldertemplate_folderをVueプロジェクトをビルドしたときに生成されるdistディレクトリを参照するようにしておきましょう。

@app.route('/',defaults={'path':''})
@app.route('/<path:path>')
def index(path):
    return render_template('index.html')

また全てのURLにおいてindex.htmlを返すように設定しておきます。

@app.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(app.root_path, 'temporary_images') ,'favicon.ico')

@app.errorhandler(500)
def something_error(error):
    return jsonify({'status': 'error'})

favicon.icoが表示されるように設定し、500エラーが起きた時にフロント側にレスポンスを返すよう、簡易的なエラーハンドリングを設定します。

favicon.icoは別に書かなくても動作すると思います。


肝心のフロント側から送られてきたデータを受け取る部分ですが

@app.route('/resolution', methods=['GET', 'POST'])
def uploads_file():
    if request.method == 'POST':
        print('requestを受け取りました。')

        isTTA = request.form['TTA']
        no_make_low = request.form['low_off']
        model_id = request.form['model_id']
        upload_image_file = request.form['image']

request.form[キー]で取得できます。要注意ポイントとしてisTTAno_make_low'true','false'といった文字列になり、pythonでのTrue, Falseと表記が違うので

if isTTA == True: # これはダメ

if isTTA == 'true': # これなら大丈夫

のように書く必要があります。

またmodel_idは1とか2とかの整数値なので、int型だろうと思ったらstring型だったので

if model_id == 1: # これはダメ

if model_id == '1': # これなら大丈夫

のように書くと動きます。

upload_image_fileはbase64型の画像になるので、以下のコードでndarray形式などに変換しています。

utils.pyの中のmake_LR_img関数
def make_LR_img(base64_file, low_off='false'):
    code = base64.b64decode(base64_file.split(',')[1]) # 先頭の余計な部分を省く
    upload_img = Image.open(BytesIO(code))
    upload_img = np.asarray(upload_img)
    HR_img = cv2.cvtColor(upload_img, cv2.COLOR_RGBA2RGB)
    if low_off == 'true':
        return HR_img

    height, width, _ = HR_img.shape
    LR_img = cv2.resize(HR_img, (width//4, height//4), interpolation=cv2.INTER_CUBIC)

    save_low_img = Image.fromarray(LR_img)
    save_low_img.save('./back/temporary_images/low_img.png', quality=100)

    return LR_img

※base64形式は
data:image/png;base64, <ランダムな文字列>のようになっており、読み込む際にはランダムな文字列の部分が欲しいので、split(',')[1]で変数codeに代入しています。

こうしてndarray形式の画像をモデルに突っ込むと、一時的に保管しておくtemporary_imagesディレクトリに、low_img.pngsr_img.pngの2つの画像が生成されています。

この画像をコチラを参考にPILで読み込んでbase64形式に変換してからフロント側に返します。

utils.pyのPIL_to_base64関数
def PIL_to_base64(image_path):
    image = Image.open(image_path)
    buffer = BytesIO()
    image.save(buffer, format="PNG")
    base64_image = base64.b64encode(buffer.getvalue()).decode().replace("'", "")

    return base64_image

この時のbase64_imageにはbase64形式の先頭部分のdata:image/png;base64,は含まれていません。なので

return jsonify({'low_img_url': 'data:image/png;base64,'+ base64_low_img,
                        'sr_img_url': 'data:image/png;base64,'+ base64_sr_img})

先頭にその部分をくっつけてフロント側に返すようにします。

これで画像の送受信の実装ができました。

5. デプロイ

Procfileの用意

Procfileには以下のように記載します。

Procfile
web: gunicorn -b 0.0.0.0:$PORT --pythonpath back back.server:app

gunicornで立ち上げるので

pip install gunicorn

でインストールしておきましょう

runtime.txtの用意

pythonランタイムの指定をします。以下のように記載。自分のpythonのバージョンにしてください。

runtime.txt
python-3.8.12

requirements.txtの用意

python側で必要なパッケージを定義しておきます。

requirement.txt
flask
numpy
opencv-python-headless
Pillow
--find-links https://download.pytorch.org/whl/torch_stable.html
torch==1.9.0+cpu
gunicorn
timm

ポイントになるのはこの部分。

--find-links https://download.pytorch.org/whl/torch_stable.html
torch==1.9.0+cpu

普通にtorchだけ記載するとGPU対応したtorchがインストールされてしまい、余裕で無料枠のSlugsizeを超えます。なのでcpu版をインストールするようにします。

コチラが参考になりました。

あとはSwinIRのモデルにtimmのレイヤーを使っているのでtimmも忘れないように。

ルートディレクトリにて

$ heroku login
$ heroku create アプリ名

でアプリを作成し

git push heroku main

でデプロイできるはずです。

機械学習を使ったアプリをデプロイする際の問題で多いのは
・パッケージのインストール不足(requirements.txtへの記載忘れ)
・slugsize オーバー(tensorflow, pytorchをGPU版でインストールしてると容量大きくなる)

だと思います。

heroku logs --tailコマンドでlogを見てエラー内容を確認するのが大事です。
自分はエラー原因のほとんどが、必要なパッケージのインストール不足でした。

終わりに

ここまで読んで頂きありがとうございます。Vue.jsとFlaskの連携方法、データの送受信に関して色々詰まりながらも実装できました。同じように連携させたい方の参考になれば幸いです。

現状、メモリオーバー時のエラーハンドリング処理など実装できてないので、
今後もう少しVue.js勉強して機能追加など改良してみたいと思います。

26
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?