Help us understand the problem. What is going on with this article?

鼓膜の画像を送り質問に返答すれば、自動で中耳炎の診断や治療方針が返されるLINE Botを作成(ヒーローズ・リーグ2019 LINEテーマ賞)

概要

耳鼻咽喉科の開業医をしています。

以前、質問に答えていくと急性中耳炎の重症度が分かるLINE Botと
鼓膜画像を送ると正常か中耳炎かを答えてくれるLINE Botを作成しました。

急性中耳炎の重症度が分かるLINE Botの作成
Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

今回、二つのBotを組み合わせて、鼓膜の画像を送り質問に返答すれば、自動で中耳炎の診断や治療方針が返されるLINE Botを作成しました。

概念図

image.png

完成動画/画像

image.png

IMG-0982.PNG

作成

以前の作成したBotのコードを変えていきます。
Azure Custom Vision ServicesのPrediction APIの発行の仕方もこちらの記事を参考にして下さい。

Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成

まず、ユーザーから送られてくるのがメッセージか画像かで処理を分けます。

function handleEvent(req, res) {
    if (req.body.events[0].type === 'message' && req.body.events[0].message.type === 'text') {
      return handleTextEvent(req.body.events[0]);
    }else if(req.body.events[0].message.type === 'image'){
      return handleImageEvent(req.body.events[0]);
    }
    console.log("サポートされていないメッセージです");
}

鼓膜画像が送られてきたときの処理です。
最も確率が高い診断名とその確率が表示されます。
診断が急性中耳炎の場合は重症度判定に必要な「鼓膜の発赤」「鼓膜の腫脹」「耳漏」の程度を確率で表示し重症度スコアを計算します。
その後年齢に関する質問が開始され、クイックリプライで表示されます。

function handleImageEvent(event) {
  console.log("画像が来たよ");
  // ユーザーがLINE Bot宛てに送った写真のURLを取得する
  const options = {
    url: `https://api.line.me/v2/bot/message/${event.message.id}/content`,
    method: 'get',
    headers: {
        'Authorization': 'Bearer 自分のchannelAccessToken'  ,
    },
    encoding: null
  };

Request(options, function(error, response, body) {

    if (!error && response.statusCode == 200) {
        //保存

        console.log(options.url + '/image.jpg');
        let strURL = options.url + '/image.jpg';

        //Nowでデプロイする場合は、/tmp/のパスが重要
        fs.writeFileSync(`/tmp/` + event.message.id + `.png`, new Buffer(body), 'binary');

        const filePath = `/tmp/` + event.message.id + `.png`;

//Azure Custom Vision APIの設定
const config = {
  "predictionEndpoint": "ひかえておいたURL",
  "predictionKey": 'ひかえておいたKey'
  };

  let result1;

  cv.sendImage(
      filePath,
      config,
      (data) => {
        console.log(data); 
          let result0="";
          // let result1;
          let result2 = "";
          let result3 = "";
          let result4 = "";
          let result5 = "";
          let strName = "";
          let Probability ;
          let strProbability;        

          for (var i = 0; i <4; i++) {
            strName = data.predictions[i].tagName;
            Probability = data.predictions[i].probability * 100;
            strProbability = Probability.toFixed();
              if (strName == "急性中耳炎") {
                result1 = "急性中耳炎";
                result0 = "ですね。\n確率は"+strProbability + '%\n\n';
              }else if (strName == "滲出性中耳炎") {
                result1 = "滲出性中耳炎";
                result0 = strProbability + '%';
              }else if(strName == "正常鼓膜") {
                result1 = "正常鼓膜"; 
                result0 = strProbability + '%';
              }
          }

          let symptoms = {};
          let score = 0;
        if (result1 == "急性中耳炎") {
          for (var i = 0; i < 10; i++) {
            strName = data.predictions[i].tagName;
            Probability = data.predictions[i].probability * 100;
            strProbability = Probability.toFixed();
            if (symptoms["発赤"] === undefined) {
              if (strName == "発赤:なし") {
                symptoms["発赤"] = "発赤なし" + strProbability + '%,\n';
                //score0
              } else if (strName == "発赤:一部") {
                symptoms["発赤"] = "発赤一部" + strProbability + '%,\n';
                score += 2;
              } else if (strName == "発赤:全体") {
                symptoms["発赤"] = "発赤全体" + strProbability + '%,\n';
                score += 4;
              }
              result2 = symptoms["発赤"];
            }

            if (symptoms["腫脹"] === undefined) {
              if (strName == "腫脹:なし") {
                symptoms["腫脹"] = "腫脹なし" + strProbability + '%,\n';
              } else if (strName == "腫脹:一部") {
                symptoms["腫脹"] = "腫脹一部" + strProbability + '%,\n';
                score += 4;
              } else if (strName == "腫脹:全体") {
                symptoms["腫脹"] = "腫脹全体" + strProbability + '%,\n';
                score += 8;
              }
              result3 = symptoms["腫脹"];
            }

            if (symptoms["耳漏"] === undefined) {
              if (strName == "耳漏:なし") {
                symptoms["耳漏"] = "耳漏なし" + strProbability + '%,\n';
              } else if (strName == "耳漏:あり") {
                symptoms["耳漏"] = "耳漏あり" + strProbability + '%,\n';
                score += 2;
              }
              result4 = symptoms["耳漏"];

            }
          }
          // }


          client.replyMessage(event.replyToken, {

              "type": "text", // ①
              "text": result1 + result0 + result2 + result3 + result4 + "➡重症度スコア:" + String(score)+"\n\nいくつか質問にお答えください。\n\n2歳未満ですか?",
              "quickReply": {
                "items": [
                  {
                    "type": "action",
                    "action": {
                      "type": "message",
                      "label": "いいえ",
                      "text": "2歳以上 トータルスコア:" + String(score)
                    }
                  },
                  {
                    "type": "action",
                    "action": {
                      "type": "message",
                      "label": "はい",
                      "text": "2歳未満 トータルスコア:" + String(score + 3)
                    }
                  }

                ]
              }            
          });

        } else if (result1 == "滲出性中耳炎") {
          client.replyMessage(event.replyToken, {
            type: 'text',
            text: result1 + "ですね。\n確率は" + result0  ,
          });
        } else if (result1 == "正常鼓膜") {
          client.replyMessage(event.replyToken, {
            type: 'text',
            text: result1 + "ですね。\n確率は" + result0 ,
          });
        }
          try {
                    fs.unlinkSync(filePath);
                    return true;
                  } catch(err) {
                    return false;
                  }
                return; 
            },
            (error) => { console.log(error) }
        );
    } else {
        console.log('imageget-err');
    }
});
}

メッセージに対する処理は、function handleTextEvent(event) { }の中に
急性中耳炎の重症度が分かるLINE Botの作成のLINE botのプログラムを入れて追記すると完成です。

質問に対するクイックリプライの回答から重症度スコアを加算していき、すべての質問が終わるとトータルスコアから急性中耳炎の重症度を判定し、ガイドラインで推奨されている治療を返します。

考察

鼓膜の画像さえきれいに撮影できれば、高い精度で急性中耳炎のガイドラインに沿った診断と推奨治療を返すBotを作成できました。

こちらのBotで昨年末に開催された開発コンテストのヒーローズ・リーグ2019で賞(LINEテーマ賞 by LINE株式会社様)をいただきとても嬉しかったです。

また、先日耳鼻咽喉科の学術講演会でこのBotについても発表させていただき耳鼻咽喉科の先生方からもかなり反響がありました。

鼓膜の撮影をするカメラ(デジタル耳鏡)は通販で3~4000円で購入できるため、一般の方が自宅で撮影することもできるのですが、Botが病気の診断することは現在の法律で禁じられているため、こちら公開して使って頂くことは出来ません。データを増やし精度を上げながら自院で医師の指導のもと中耳炎の再来患者さんを中心に使用していただいて、有効性や安全性を検証していきたいと思っています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした