pythonで果物分類してみた③モデル検討編

#7.Flaskアプリケーション化

・　**Python+FlaskでのWebアプリケーション開発**


Flaskにログイン
https://flask-login.readthedocs.io/en/latest/


###Flaskとは

* Flaskとは、Pythonのための軽量なウェブアプリケーションフレームワークです。
フレームワークとは、アプリケーションソフトを開発する際に必要とされる機能をまとめて整えてくれている枠組み・ひな形を簡単に構築できるパッケージのことを指します。

* Flaskを用いてサーバとの処理を行い、HTML&CSSでアプリの見た目を作ります。

#Flaskモジュールのインストール
pipで拡張機能をインストールします。

```haml
pip install flask-login
```
#Flaskウェブアプリケーションを実行してみる

```python
hello.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello World!"

if __name__ == "__main__":
    app.run()
```
#コード解説
* **flaskパッケージのFlaskクラスを使用することを宣言します。**

```python
from flask import Flask
```

* **appというFlaskクラスのインスタンスを作成します。**

app. (Flaskクラスのメソッド名) とすることでFlaskクラスのメソッドを使えるようにします。

```python
app = Flask(__name__)
```

* **@で始まる行はデコレータといって、その次の行で定義する関数やクラスに対して何らかの処理を行います。app.route()は、次の行で定義される関数を指定した URL に対応づけるという処理をしています。**

```python
@app.route('/')#http://127.0.0.1:5000/ 以降のパスを指定する
def hello_world():
    return "Hello World!"
```


* __name__ == '__main__'がTrueである、すなわちこのコードが直接実行されたときのみapp.run()が実行され、Flaskアプリが起動します。

```python
if __name__ == "__main__":
    app.run()
```

* ``if __name__ == '__main__':``の記述によって、直接そのPythonスクリプトを実行したとき(コマンドラインでpython ファイル名.pyを実行した時のように)のみ、``if __name__ == '__main__':``以下の処理を実行させることができます。そして、他のファイルからインポートされた場合には処理を実行しません。







#ファイルフォルダtemplates/

###HTML&CSSとは

* HTMLとはHyper Text Markup Language（ハイパーテキストマークアップランゲージ）の略で、ホームページなどを作成する際に用いられます。文書構造を定義するものであり、骨組みや構造となるマークアップ言語です。ほぼすべてのウェブページにはHTMLが使われています。

* CSSはCascading Style Sheets（カスケーディング・スタイル・シート）の略で、ホームページなどの見た目・スタイルを定義するための言語のことです。HTMLで画面に表示する文書構造などを定義し、CSSで表示方法や装飾などを定義します。

* ローカルの環境で行ってください。

**HTMLファイルを格納する場所**


```python:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>アプリ作成</title>
    <link rel="stylesheet"  href="./static/style.css" > 
    <script src="https://code.jquery.com/jquery-3.4.0.min.js"
    integrity="sha256-BJeo0qm959uMBGb65z40ejJYGSgR7REI4+CW1fNKwOg=" crossorigin="anonymous"></script>
</head>
<body>
  <div id="main" class="site-width">
      
       <!--<ul class="slider__container">
    
            <img src="https://www.dole.co.jp/banana/nutrients/assets/images/eiyo_img01.png" alt="" class="slider__item slider__item1 slider__item1">
            
            <img src="https://www.pakutaso.com/shared/img/thumb/PED_narandaapple2_TP_V.jpg" alt="" class="slider__item slider__item1">

            <img src="https://image.rakuten.co.jp/rainbow246/cabinet/02541763/03210664/imgrc0070019412.jpg" alt="" class="slider__item slider__item1">

          </ul> -->

          <form class="box" action="predict" method="post">
            <h1>Login</h1>
            <input type="text" name="name" placeholder="Username">
            <input type="password" name="pass" placeholder="Password">
            <input type="submit" >
          </form>
  </div>
   
</body>
<script src="./static/app.js"></script>

</html>
```

* この部分はDOCTYPE宣言と呼ばれるもので、テキストファイルはHTMLだとブラウザに説明するための宣言

```python

<!DOCTYPE html>
```

* この部分は、このHTMLは英語を扱うとブラウザに説明するための宣言です。今回は日本語を扱うので先ほどのように"ja"と書き換えます。

```python
<html lang="ja">
```

* htmlでは下のようなタグと呼ばれる見出しをつけて、タグに囲まれたテキストやコードに様々な意味を持たせます。また、タグに囲まれたテキストやコードのことを要素といいます。

```python
<head></head>
```

* このまとまりはheadタグに囲まれているのでhead要素といいます。head要素の中にmetaタグやtitleタグ、CSSの読み込みを記入したりします。また、CSSの読み込みをする部分は自動で補完されないのでhead要素の最後に加えました。それでは順番に見ていきましょう。

```python
<head>
    <meta charset="UTF-8">
    <title>アプリ作成</title>
    <link rel="stylesheet"  href="./static/style.css" > 
    <script src="https://code.jquery.com/jquery-3.4.0.min.js"
    integrity="sha256-BJeo0qm959uMBGb65z40ejJYGSgR7REI4+CW1fNKwOg=" crossorigin="anonymous"></script>
</head>
```
この部分は、このHTMLの文字コードはUTF-8だとブラウザに説明するための宣言です。この指定が違っていると文字化けの原因になります。

```python

<meta charset="UTF-8">
```

title要素はページのタイトルを指定します。ここで指定したタイトルは、ブラウザのタブ上に現れます。

```python
<title>アプリ作成</title>
```

CSSの読み込みをする部分です。href=""にCSSファイルの置いている場所とファイル名を渡します。

```python
<link rel="stylesheet" href="./static/style.css">
```

**以上でhead要素は終わりです。**


##メイン
div要素の「div」は「division」の略で、要素をグループ化するために使用されます。また、各要素をCSSで扱いやすいようにclass="xxx"としてラベリングしています


```python

<div id="main" class="site-width">

       <!--<ul class="slider__container">

            <img src="https://www.dole.co.jp/banana/nutrients/assets/images/eiyo_img01.png" alt="" class="slider__item slider__item1 slider__item1">

            <img src="https://www.pakutaso.com/shared/img/thumb/PED_narandaapple2_TP_V.jpg" alt="" class="slider__item slider__item1">

            <img src="https://image.rakuten.co.jp/rainbow246/cabinet/02541763/03210664/imgrc0070019412.jpg" alt="" class="slider__item slider__item1">

          </ul> -->

          <form class="box" action="predict" method="post">
            <h1>Login</h1>
            <input type="text" name="name" placeholder="Username">
            <input type="password" name="pass" placeholder="Password">
            <input type="submit" >
          </form>
  </div>
```


見出しの要素です。h1>h2>h3>h4>h5>h6の順で重要度が高くなります。

```python
        <h1>Login</h1>
```

この部分はファイルの選択と送信ボタンについての記述です。入力フォームを作成するときにはform要素を使います。ここで``class="box" action="predict" method="post"``と指定していますが、これはboxリクエストで画像をアップロードするということを指します。すなわち、Flask入門で扱っている``request.methodに"box"``が渡され、``if``文が``True``になるためFlask側で処理が進められます。また、ファイルを送信するフォームでは``action="predict" method="post">``と指定しましょう。


```python
<form class="box" action="predict" method="post">
            <h1>Login</h1>
            <input type="text" name="name" placeholder="Username">
            <input type="password" name="pass" placeholder="Password">
            <input type="submit" >
          </form>
```

ファイルのアップローダーを作成するときは``input要素``に``type="text"``と指定します。また``name="name"``の``"name"はFlask入門``で扱った``request.files``に渡されています。

```python
<input type="text" name="name" placeholder="Username">
<input type="password" name="pass" placeholder="Password">

```

送信ボタンの部分です。typeには"submit"を指定します。valueには送信ボタンに書かれる文字を指定します。

```python
<input type="submit" >
```


##:補足
基本的には文章を入力する際に使います。パラグラフのpであり、１つの段落を意味します。
改行には br を使いましょう


```python

<p>送信してください</p>
```






#static/
・　HTMLファイル以外の、CSSファイル・JSファイル・画像ファイル等を格納する場所


・　モバイルWebアプリを作成するための、軽量なJavaScript UIライブラリです

```python:app.js
//最初にどのスライドから表示するかを選択
var currentItemNum = 1;

var $slideContainer = $('.slider__container');

//クラスslider__itemの要素の個数を算出
var slideItemNum = $('.slider__item').length;

//スタイド１つあたりの横幅を算出
var slideItemWidth = $('.slider__item').innerWidth();

//スライド１つあたりの横幅とスライドの個数から
var slideContainerWidth = slideItemWidth * slideItemNum;

//スライドさせる速さ(animateメソッド用)
var DURATION = 1000;

//横に並べた複数のスライドを格納する用コンテナの横幅を設定
$slideContainer.attr('style', 'width:' + slideContainerWidth + 'px');

//スライドを自動的に変化させる関数
function autoSlide(){
    //現在のスライドの番号が全部のスライドの数より少ないとき
    if(currentItemNum < slideItemNum){
        //スライド用コンテナをanimateメソッドで変化させる
        $slideContainer.animate({left: '-='+slideItemWidth+'px'}, DURATION);//現在のCSSのleftのプロパティの値からスライド１つあたりの横幅分変化させる
        currentItemNum++;//現在のスライド番号を１増やす
    //現在のスライドの番号が全部のスライドの数と同じとき
    }else if(currentItemNum = slideItemNum){
        //現在のCSSのleftのプロパティの値を変更(徐々に変化させないのでanimateメソッドは使わない)
        $slideContainer.css('left', '+='+slideItemWidth*(slideItemNum-1)+'px');
        currentItemNum = 1;//現在のスライド番号を１に戻す
    }
}



//setInterval関数で適当な秒数ごとに実施
setInterval(autoSlide, 5000);


//logoutボタン
$('#logout').on('click', function() {
    window.location.href ="/logout"
});
```

```python:style.css
body{
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  background: #34495e;
}

/*ログインフォームのエリア内のスタイル*/
.box{
  width: 300px;
  padding: 40px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #191919;
  text-align: center;
}

.box{
  color: white;
  text-transform: uppercase;
  font-weight: 500;
}

/* ユーザーIDとパスワード入力ボックスのスタイル */
.box input[type="text"], .box input[type="password"]{
  border: 0;
  background: none;
  display: block;
  margin: 20px auto;
  text-align: center;
  border: 2px solid #3498db;
  padding: 14px 10px;
  width: 200px;
  outline: none;
  color: white;
  border-radius: 24px;
  transition: 0.25s;
}

/* 入力ボックスがフォーカスされたときのスタイル（幅が大きくなる＆枠の色が変わる） */
.box input[type="text"]:focus, .box input[type="password"]:focus{
  width: 280px;
  border-color: #2ecc71;
}

/* Loginボタンのスタイル */
.box input[type="submit"]{
  border: 0;
  background: none;
  display: block;
  margin: 20px auto;
  text-align: center;
  border: 2px solid #2ecc71;;
  padding: 14px 40px;
  outline: none;
  color: white;
  border-radius: 24px;
  transition: 0.25s;
  cursor: pointer;
}

/* Loginボタンをマウスオーバーしたときのスタイル */
.box input[type="submit"]:hover{
  background: #2ecc71;
   
}

/* Looutボタンのスタイル */
.box input[type="button"]{
  border: 0;
  background: none;
  display: block;
  margin: 20px auto;
  text-align: center;
  border: 2px solid #ff9100;;
  padding: 14px 40px;
  outline: none;
  color: white;
  border-radius: 24px;
  transition: 0.25s;
  cursor: pointer;
}

/* Looutボタンをマウスオーバーしたときのスタイル */
.box input[type="button"]:hover{
  background:  #ff9100;
   
}



body{
  background: #000000;
  color: #333;
}
.main{
  width: 1000px;
  margin: 30px auto;
}

.slider{
  /* スライドを表示させる画面の横幅(≒スライド１枚あたりの横幅) */
  width: 1000px;
  /* スライドを表示させる画面の縦幅(≒スライド１枚あたりの縦幅) */
  height: 500px;
  /* はみ出した要素は隠れるようにする */
  overflow: hidden;
}
.slider__container{
  /* スライドを表示する画面とスライド１枚をぴったり合わせるため設定 */
  padding: 0;
  margin: 0;
  
  /* 子要素でfloatを利用しているため設定 */
  overflow: hidden;

  list-style: none;

  /* 後ほどJSでleftプロパティを設定するため必要 */
  position: relative;

}
.slider__item{
  /* スライド１枚あたりの横幅 */
  width: 1000px;
  /* スライド１枚あたりの縦幅 */
  height: 500px;
  /* スライドを横一列に並べる */
  float: left;
}
```

```main.py
import os
from flask import Flask, request, redirect, url_for, render_template, flash ,session
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image
import numpy as np

classes = ["リンゴ","バナナ","オレンジ"]
num_classes = len(classes)
image_size = 50

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

name ="test"
password = "0000"

app = Flask(__name__)
app.secret_key = "test_aidemy"

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

#model = load_model('./model.h5')
model = load_model('./traning.h5')
# ログイン画面
@app.route('/', methods=['GET', 'POST'])
def login_func():
    if "flag" in session and session["flag"]:
        return redirect("/predict")
    else:
        return render_template("index.html")

# ログアウト
@app.route('/logout', methods=['GET', 'POST'])
def logout_func():
    session.pop('flag', None)
    return redirect("/")

#学習済みモデルをロードする
@app.route('/predict', methods=['GET', 'POST'])
def upload_file():
        #　ログインチェック
        if "name" in request.form and "pass" in request.form:
            if request.form["name"] == name and request.form["pass"] == password :
                session["flag"] = True
        # file　チェック
        if ("flag" in session and session["flag"])==False :    
            return redirect("/")
        
        if 'file' not in request.files:
            return render_template("predict.html")  
           
        file = request.files['file']
        if file.filename == '':
            return render_template("predict.html") 
        if file and allowed_file(file.filename):
            
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = (os.path.join(UPLOAD_FOLDER, filename))

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            
            predicted = result.argmax()
            #pred_answer = "これは" + classes[predicted] + "です"
            
            return render_template("predict.html",answer=predicted)

        return render_template("predict.html")



if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)
```
##:補足
``if __name__ == '__main__':``の記述によって、直接そのPythonスクリプトを実行したとき(コマンドラインでpython ファイル名.pyを実行した時のように)のみ、``if __name__ == '__main__':``以下の処理を実行させることができます。


``__name__``というのはスクリプトファイルごとに自動的に定義される変数で、そのファイル名（モジュール名）が自動的に格納されます。ただし、ファイルを直接実行したときには``__name__``に``__main__``が自動的に格納されます。

**test1.py**

```python
#関数show()を定義しておく
def show():
    return __name__

print(__name__)
#出力結果
__main__
```
``test1.py``が直接実行されているため``__name__``に``__main__``が自動的に格納されるので、``__main__``と出力 されます。

Pythonスクリプトが実行された時には``__name__``という変数に``__main__``が格納されるため、``__name__ == '__main__``がTrueとなり`if __name__ == '__main__':`以下の処理が実行されます。




#requirements.txtの作成

* requirements.txt に必要なライブラリを記述することでHerokuの自分のアプリ内にそのライブラリがインストールされます。
runtime.txtには使用するPythonのバージョンを記述します。

(verなどを合わせてファイルの中身を書き直してください。)


```python

absl-py==0.9.0
astor==0.8.1
bleach==3.1.5
bottle==0.12.18
click==7.1.2
certifi==2020.6.20
chardet==3.0.4
flask>=1.1.2
future==0.18.2
gast==0.3.3
grpcio==1.31.0
gunicorn==20.0.4
h5py==2.10.0
html5lib==1.1
itsdangerous==1.1.0
idna==2.10
Jinja2>=2.11.2
Markdown==3.2.2
MarkupSafe==1.1.1
numpy==1.18.0
oauthlib==3.1.0
pillow==7.2.0
protobuf==3.12.4
PyYAML>=5.3.1
python-dotenv==0.14.0
requests>=2.24.0
scipy==1.4.1
six==1.15.0
tensorboard==2.3.0
tensorflow==2.3.0
keras==2.4.3
termcolor==1.1.0
urllib3>=1.25.10
Werkzeug==1.0.1
```

#公開設定
VScodeでwebアプリを動かしていました。ここではwebアプリをHerokuへデプロイし、全世界に公開する手順を学習します。

webアプリをデプロイする際には、サーバーを外部からも利用可能にするために``host='0.0.0.0'``と指定します。``port = int(os.environ.get('PORT', 8080))``では、Herokuでの使えるポート番号を取得してportに格納しています。設定されていなければ``8080``が格納されます。

**mnist.pyの末尾の**

```python
if __name__ == "__main__":
    app.run()
```

**の部分を**

```python
if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)
```









#Webサーバを立てる
以下のコマンドをターミナルで実行するとWebサーバが起動します。


```python

python main.py
```
するとターミナルに以下のようなメッセージが出るので、


```
Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
```
ブラウザで　http://0.0.0.0:8080/　を開きましょう。
![スクリーンショット 2021-02-04 23.06.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942550/1d374696-d70c-753c-cf32-3a98ec9a0c57.png)



#④へ続く　↓