9
5

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.

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

Last updated at Posted at 2020-01-21

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

以前、質問に答えていくと急性中耳炎の重症度が分かる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が病気の診断することは現在の法律で禁じられているため、こちら公開して使って頂くことは出来ません。データを増やし精度を上げながら自院で医師の指導のもと中耳炎の再来患者さんを中心に使用していただいて、有効性や安全性を検証していきたいと思っています。

9
5
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
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?