歯医者(!?)がflask+kerasで舌苔量判定アプリを作成してみた

#はじめに
keras+flaskにより、舌の画像から汚れ度合いを判定するwebアプリ(アプリ名：Tanpic)を作成しました。

開発したきっかけは、舌苔(ぜったい)と呼ばれる舌表面の汚れが口臭と関係していると言われており(*1)、スマホで口臭を可視化できるようなアプリがあれば面白いのではと思ったためです。


作成にあたってudemyの<a href="https://px.a8.net/svt/ejp?a8mat=35RZ3H+G23XY2+3L4M+BW8O2&a8ejpredirect=https%3A%2F%2Fwww.udemy.com%2Fcourse%2Ftensorflow-advanced%2F" rel="nofollow">【画像判定AIアプリ開発・パート1】TensorFlow・Python・Flaskで作る画像判定AIアプリ開発入門</a><img border="0" width="1" height="1" src="https://www13.a8.net/0.gif?a8mat=35RZ3H+G23XY2+3L4M+BW8O2" alt="">をかなり参考にさせていただきました。解説も分かりやすく、質問もできるため、初めて画像分類アプリを作るという方にはオススメの教材かと思います。

ちなみに私は現在歯学部の5年生でして、実際にはまだ歯医者ではありません。。。



#成果物


<img width="300" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/47da754b-ebd6-e7fc-7bea-2a6d13e471cb.png">


舌の画像をアップロードすると...



<img width="300" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/498ea483-5c47-97b2-4ea1-7a8af538c0eb.png">


舌の汚れ度合いを3段階で判定。ついでに口臭に関するちょっとした豆知識を返してくれます。挙動の悪さや、デザインセンスのなさは大目に見ていただけたらと思います。。。

Tanpic:https://tanpic-b86b4.firebaseapp.com/



#データ収集
このフェーズでは以下の作業を行いました。

1. icrawlerで画像の収集
2. 不要な画像の排除
3. トリミング
4. 舌の汚れ度合いに応じて3段階にラベル付け


画像データはicrowlerを使って収集しました。詳細はドキュメントを参考にしてください。

参考：[icrawler Documentation](https://icrawler.readthedocs.io/en/latest/)




 


不要な画像を排除したあと、舌の部分のみを１枚１枚手作業でトリミング。その後、舌の汚れ度合いを3つに分類しました。

汚れ度合い1(ラベル名:tongue0)
<img width="300" alt="スクリーンショット 2019-11-01 19.11.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/5eb3b2b3-88e7-25ca-f0d0-996943b884fe.png">




汚れ度合い2(ラベル名:tongue1)
<img width="300" alt="スクリーンショット 2019-11-01 19.14.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/1efba772-9443-b06c-fba2-bb37fd620cad.png">




汚れ度合い3(ラベル名:tongue2)
<img width="300" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/8ed83b20-8251-e226-006d-a15967774f9b.jpeg">

データの処理は時間も手間もかかってかなり大変でした汗



#学習
学習にはgooglecolabratolyのGPUを使いました。

収集した画像を水増ししてTrainingした後、モデルをtongue_cnn_aug.h5として保存。詳細は割愛させていただきます。

#Test Accuracy

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/5dd6f8cc-1084-b8cf-52ae-6bfe20d4431f.png)


66.66%となりました。

#flaskとの連携

行なっていることを以下に簡単にまとめます。

1. modelをロード
2. imageをRGBで3色に変換
3. imageを50*50にリサイズ
4. imageをNumpy Arrayに変換
5. imageをmodelに渡しpredict
6. argmax関数で、予測されたラベルの値を返す
7. render_template関数で、htmlファイルにラベル値を渡す


```predict_web.py

import os
from flask import Flask, flash, request, redirect, url_for, render_template
from werkzeug.utils import secure_filename

from keras.models import load_model
import numpy as np
from PIL import Image
import tensorflow as tf


class_1 = "1です。"
class_content_1 = "舌苔はほとんどついておらず、かなり綺麗な状態かと思われます。"
classes_solution_1 = "これまでと同様に、口腔ケアを頑張っていきましょう。注意点として、舌の掃除のやりすぎは舌を傷つけるだけでなく舌苔の付着量増加にもつながるため、１日１回を限度に優しく行うようにしましょう。"

class_2 = "2です。"
class_content_2 = "舌の汚れは気にする必要はないでしょう。"
classes_solution_2 = "舌の汚れ(舌苔)は口臭の原因にもなるため、もし気になるようでしたら一日一回を目安に舌を優しく掃除してあげるといいでしょう。また、唾液の分泌が少なくなる夕方の時間帯は口臭が強くなる傾向があります。うがい、水分補給をして対策するようにしてください。"

class_3 = "3です。"
class_content_3 = "舌に汚れが若干多くついているかもしれません。舌苔（舌の汚れ）は口臭の原因にもなるため、普段の歯磨きに加え、舌を掃除することが望ましいでしょう。"
classes_solution_3 = "舌苔の掃除法としてはガーゼや舌ブラシを用いて、舌の表面を奥から手前に優しくこするようにして行ってください。1日1回を限度に行いましょう。"

classes = [class_1,class_2,class_3]
classes_content = [class_content_1, class_content_2, class_content_3]
classes_solution = [classes_solution_1, classes_solution_2, classes_solution_3]


num_classes = len(classes)
image_size = 50

UPLOAD_FOLDER = './uploads'
ALLOWED_EXTENSIOS = set(['png', 'jpg', 'gif','heic'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER



model = load_model('./tongue_cnn_aug.h5')
graph = tf.get_default_graph()



def allowed_file(filename):
    #もし.が含まれたる　かつ　もし拡張子前の.で区切り、拡張子小文字小文字であれば　true
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIOS

# @app.route('/', methods=['GET', 'POST'])
@app.route('/')
def upload_file():
        return render_template('test.html')



@app.route('/predict',methods=['GET', 'POST'])
def predict_file():
    global graph
    with graph.as_default():
        if request.method == 'POST':

            file = request.files['file']  # 追加
            if file and allowed_file(file.filename):
                filename = secure_filename(file.filename)
                file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
                filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)

                image = Image.open(filepath)
                image = image.convert('RGB')
                image = image.resize((image_size, image_size))
                data = np.asarray(image)
                X = []
                X.append(data)
                X = np.array(X)

                result = model.predict([X])[0]
                predicted = result.argmax()

                return render_template('predict.html', result = classes[predicted], result_content = classes_content[predicted], result_solution = classes_solution[predicted], result_title="解析結果",img_name=file.filename)


from flask import send_from_directory

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

```


```predict.html
<!DOCTYPE! html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>Tanpic</title>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link rel="stylesheet" href="/static/css/style.css">
        <link rel="shortcut icon" href="/static/favicon.ico">

        <!-- BootstrapのCSS読み込み -->
        <link rel="stylesheet" href="/static/css/bootstrap.min.css">

        <!--manifest.jsonの読み込み-->
        <!--<link rel="manifest" href="./manifest.json">-->
    </head>

    <body>
        <!--navi bar-->
        <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
          <a class="navbar-brand" href="#">Tanpic</a>

          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>


          <div class="collapse navbar-collapse" id="navbarTogglerDemo03">
            <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
              <li class="nav-item active">
                <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
              </li>
              <li class="nav-item">
                <a class="nav-link" href="#">Link</a>
              </li>
              <li class="nav-item">
                <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
              </li>
            </ul>
          </div>
        </nav>

        <!--header area-->
        <header class = "bg-primary text-center">
            <div class = "bg-mask">
                <div class="container">
                    <h1>Tanpic</h1>
                    <h2>舌の写真を載せてみてね</h2>
                    <ul>
                        {% for entry in entries %}
                        <li>{{entry.title}} / {{entry.text}}</li>
                        {% endfor %}
                    </ul>
                </div>
            </div>
        </header>


        <!--sorting Grid section-->
        <section class = "sorting">
        <div class = "container text-center">
            {%if img_name%}
                <img src="uploads/{{img_name}}" style="width:300px;height:300px;">

            {%endif%}


        </div>
        </section>
<!-- prediction session-->
        <div class = "container">
            <p>
                {%if result_title%}
                    {{result_title}}
                {%else%}
                {%endif%}

            </p>
            <p class="lead">
                <span>
                  {% if result %}
                    あなたの舌の汚れ度合いは３段階中、{{ result }}
                  {% else %}
                  {% endif %}
                </span>
                <br>
                <br>
                <span>
                    {%if result_content%}
                    {{result_content}}
                    {%else%}
                    {%endif%}
                </span>
                <br>
                <br>
                <span>
                    {%if result_solution%}
                    {{result_solution}}
                    {%else%}
                    {%endif%}
                </span>
            </p>
        <a href="/">戻る</a>
        </div>

        <!-- Footer -->
<footer class="page-footer font-small cyan darken-3">

  <!-- Footer Elements -->
  <div class="container">

    <!-- Grid row-->
    <div class="row">

      <!-- Grid column -->
      <div class="col-md-12 py-5">
        <div class="mb-5 flex-center">

          <!-- Facebook -->
          <a class="fb-ic">
            <i class="fab fa-facebook-f fa-lg white-text mr-md-5 mr-3 fa-2x"> </i>
          </a>
          <!-- Twitter -->
          <a class="tw-ic">
            <i class="fab fa-twitter fa-lg white-text mr-md-5 mr-3 fa-2x"> </i>
          </a>
          <!-- Google +-->
          <a class="gplus-ic">
            <i class="fab fa-google-plus-g fa-lg white-text mr-md-5 mr-3 fa-2x"> </i>
          </a>
          <!--Linkedin -->
          <a class="li-ic">
            <i class="fab fa-linkedin-in fa-lg white-text mr-md-5 mr-3 fa-2x"> </i>
          </a>
          <!--Instagram-->
          <a class="ins-ic">
            <i class="fab fa-instagram fa-lg white-text mr-md-5 mr-3 fa-2x"> </i>
          </a>
          <!--Pinterest-->
          <a class="pin-ic">
            <i class="fab fa-pinterest fa-lg white-text fa-2x"> </i>
          </a>
        </div>
      </div>
      <!-- Grid column -->

    </div>
    <!-- Grid row-->

  </div>
  <!-- Footer Elements -->

  <!-- Copyright -->
  <div class="footer-copyright text-center py-3">© 2018 Copyright:
    <a href="https://mdbootstrap.com/education/bootstrap/"> MDBootstrap.com</a>
  </div>
  <!-- Copyright -->

</footer>

        <script type="text/javascript" src="/static/js/jquery-3.3.1.min.js"></script>

        <!--js script-->
        <script type="text/javascript" src="/static/js/text.js"></script>

        <!-- BootstrapのJS読み込み -->
        <script type="text/javascript" src="/static/js/bootstrap.min.js"></script>

        <!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
        <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script>
        <!-- Add Firebase products that you want to use -->
        <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-firestore.js"></script>
        <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-storage.js"></script>


    </body>


</html>

```


#firebase Cloud Storage と連携
アップロードされた画像を収集するため、Firebase Cloud Storageを使うことにしました。

参考記事：[Firebase Cloud Storageへの画像アップロードアプリを実装](https://kapi-travel.com/programing/firebase-cloud-storage%E3%81%B8%E3%81%AE%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E5%AE%9F%E8%A3%85/)

```text.js

// Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
  const storage = firebase.storage();
  var file_name;
  var blob;
  
  $('#f1').on('submit',function(e){

    //画像がアップロードされずにsubmitされた時の処理
    if($('#myfile').val()===""){
      $('#error').text("ファイルを選択してください");
      e.preventDefault();
    }

    //Cloud Storageにアップロードする処理
    var uploadRef = storage.ref().child(file_name);
    uploadRef.put(blob).then(snapshot => {
      console.log(snapshot.state);
    });
    // valueリセットする
    file_name = '';
    blob = '';

  });

```



<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314213/9fb3b014-2023-dded-9d7d-076790d5b170.png">


コンソール画面を確認したところ、しっかりと画像が保存されていました。とっても簡単に実装できたので便利でした。



#herokuにデプロイ
下記の記事を参考にherokuにデプロイしました。


参考：[Qiita Flaskチュートリアル + herokuにデプロイ](https://qiita.com/yuucu/items/dea6e7a0a655af8fdb91)

herokuにログイン後、以下の手順でコミットします。

```
$ git add .
$ git commit
$ git push heroku master
```


git push heroku master時にメモリの容量不足によりR14エラーが生じ、つまづきました。Freeプランだとメモリの上限が512MBで、これを越えたためにR14エラーが起きたようです。サイズの小さいモデルを使用して、メモリ使用量を上限以下に抑えることで解決しました。

参考：[Qiita HerokuでR14/R15エラーが起こった時の対処法
](https://qiita.com/yo-iida/items/20e86e913000f6d080ca)


#ソースコード
github:https://github.com/salmon0511/TongueCoatingClassificationApp

#感想
アイデアを実現するために様々な記事を参考にしながら実装していくのは楽しかったです。また、幾度のエラーを乗り越えてデプロイできたときは感動しました。

ただ、可読性は気にしていなかったため(正確に言うと、可読性が高いコードがなんたるかがまだ分かっていない)、今度から読みやすいコードをかけるように心がけていきたいと思います。



#参考

・*1 [Characteristics of patients complaining of halitosis and the usefulness of gas chromatography for diagnosing halitosis](https://www.sciencedirect.com/science/article/pii/S1079210401416635)

・[Qiita Flask & Keras で画像分類サービスのプロトタイプ作成](https://qiita.com/harupy/items/4fc5809e962832640aa9)

・[Flask+Kerasで手書き文字認識アプリケーションを作る](https://qiita.com/KeisukeToyota/items/c1c626c9efc816932a96)