LINE Botで医薬品を瞬時に識別！ 禁忌情報や出荷状況も一目でわかる新システムのプロトタイプ

皆さんこんにちは！おもぷーです。



今回の記事は、LINE Botに医薬品の写真を送信すると、自動で薬の名前や詳細情報を返信してくれて、さらに**禁忌情報や限定出荷の有無**なども一緒に確認できるという、痒いところに手が届くプロトタイプになってます。

**Teachable Machine**、**LINE Bot**、**Google Apps Script (GAS)** を連携させたプロトタイプになります。

## 実際の動作動画
https://x.com/331824miyamoto/status/1838236234248065079

## なぜこのプロトタイプを作ったのか
現在の薬局業界では**医薬品供給不足**が大きな課題となっています。特に、入荷できない薬が増えており、どの薬が通常通り入荷でき、どの薬が入荷困難なのかを常に確認しなければなりません。

**この確認作業がとにかくめんどくさい**。

例えば、A薬とB薬が通常通り入荷していたとしても、ある日突然B薬だけが欠品し、その代替薬も入手できないケースが頻発しています。
このような状況では、薬局側は卸会社に連絡して毎回在庫状況を確認し、患者さんへの対応を再検討しなければならず、業務負担は増える一方です。業務効率が低下するだけでなく、患者さんをお待たせしてしまい、信頼の損失にもつながりかねません。


<font color="red">**調剤事務（受付）の負担も爆増しています**




https://www3.nhk.or.jp/news/html/20240618/k10014483281000.html

自分の職場の調剤事務も、１日に何回も卸に確認をしており、**この無駄な時間を何とかしないといけません**。今回考えた解決策は、LINEで薬の写真を送信し、**出荷情報を瞬時に確認できるシステム**となります。

（薬局に１錠もない場合は基本的に他薬局へ紹介をしています）

## 使用ツール

1. **Teachable Machine**
   - 画像認識モデルを作成し、薬の画像を判定するために使用
   - URL: [Teachable Machine](https://teachablemachine.withgoogle.com/)

2. **LINE Messaging API**
   - LINE Botを作成し、ユーザーが薬の写真を送信することで判定結果を返信する機能を提供
   - URL: [LINE Developers](https://developers.line.biz/ja/)

3. **Google Apps Script (GAS)**
   - 薬の詳細情報、禁忌情報、出荷情報をGoogleスプレッドシートから取得し、LINEで返信するために使用
   - URL: [Google Apps Script](https://script.google.com/)

4. **Node.js**
   - サーバーサイドの処理を実行し、LINE BotとTeachable Machine、GASを連携させるために使用
   - URL: [Node.js](https://nodejs.org/)


6. **TensorFlow.js**
   - Teachable Machineのモデルを利用して、画像の判定を行うために使用
   - URL: [TensorFlow.js](https://www.tensorflow.org/js)
## 手順
###  Teachable Machineで薬分類モデルを作成
1. Teachable Machineにアクセスし、画像プロジェクトを作成
![スクリーンショット 2024-09-23 220324.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/d4e7d34c-79af-b310-92c3-d5f29614d557.png)

2. モデルを学習させ、精度を確認![スクリーンショット 2024-09-23 220337.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/7ca38183-14f0-51e9-6dab-cebc92933cd5.png)


3. TensorFlow.js形式でエクスポートし、ダウンロード
![スクリーンショット 2024-09-23 220444.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/c0019764-e605-c903-68b2-890227dc7e8d.png)


4. ダウンロードしたモデルを保存
![スクリーンショット 2024-09-24 161126.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/0c1999bf-d0b8-4d41-9c95-207ca1e5e93b.png)

###  **LINE Messaging APIの設定**
 1. [LINE Developers](https://developers.line.biz/ja/)にアクセスし、新規チャネルを作成します
   2. 新規作成をしたら、
チャンネルの設定画面で「Messaging API」を有効にし、チャネルシークレットとチャネルアクセストークンを取得します
![スクリーンショット 2024-09-24 162301.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/3a567829-86d7-1e92-a2cd-82782aa52b1d.png)
   
   3.  WebhookのURLを設定します
![スクリーンショット 2024-09-24 162805.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/650277b5-11ad-d789-2e67-bfc572365c69.png)

###  **Google Apps Script (GAS) でスプレッドシート連携**
写真で医薬品を判別した後、その医薬品の情報をLINEに送るためGASと連携をしました

 1. Googleスプレッドシートを作成し、薬の名前、詳細情報、禁忌情報、出荷情報などを記載し、拡張機能からApps Scriptを選択します![スクリーンショット 2024-09-24 163933.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/a771e335-a87c-737f-9e48-12299fe3ac9b.png)

2.  GASスクリプトを作成

<details>
<summary>🐽クリックしてコードを表示🐽</summary>

```javascript
      function doGet(e) {
        const sheet = SpreadsheetApp.openById('スプレッドシートID').getActiveSheet();
        const query = e.parameter.query;
        const data = sheet.getRange(2, 1, sheet.getLastRow(), sheet.getLastColumn()).getValues();
        for (const row of data) {
          if (row[0] === query) {
            return ContentService.createTextOutput(JSON.stringify({
              "薬名": row[0],
              "詳細": row[1],
              "禁忌情報": row[2],
              "出荷情報": row[3]
            })).setMimeType(ContentService.MimeType.JSON);
          }
        }
        return ContentService.createTextOutput(JSON.stringify({ "error": "該当する薬が見つかりません" })).setMimeType(ContentService.MimeType.JSON);
      }
 ```
</details> 


      
**スプレッドシートID**の設定を忘れずに行ってください

3 .  デプロイして「ウェブアプリ」として公開します。
![スクリーンショット 2024-09-24 165505.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/af1b56a9-43c3-1d9a-9641-3f7216a0af94.png)
内容の設定をします
![スクリーンショット 2024-09-24 165646.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/8adfbed9-c346-f154-4de4-d4c146e1d84f.png)
**作成されたURLは後で使用します**
### **Node.jsでサーバーをセットアップ**
   1. Node.jsをインストールしていない場合、[Node.js公式サイト](https://nodejs.org/)からインストールします。
   2. プロジェクトプロンプトを作成し、以下のコマンドで必要なパッケージをインストールします
      ```bash
      npm init -y
      npm install express body-parser @line/bot-sdk node-fetch @tensorflow/tfjs-node
      ```
   3. `index.js`ファイルを作成し、以下のコードを貼り付けます
      ファイルの作成はメモ帳を使用します
      
<details>
<summary>🐽クリックしてコードを表示🐽</summary>
      
```javascript
     const express = require('express');
const bodyParser = require('body-parser');
const line = require('@line/bot-sdk');
const fetch = require('node-fetch');
const tf = require('@tensorflow/tfjs-node');

const app = express();
const port = process.env.PORT || 3000;

// LINE APIの設定
const config = {
  channelAccessToken: 'チャンネルアクセストークン',  // あなたのチャンネルアクセストークン
  channelSecret: 'シークレットトークン'  // あなたのシークレットトークン
};

// LINE SDKのクライアントオブジェクトを定義
const client = new line.Client(config);

app.post('/webhook', line.middleware(config), (req, res) => {
  console.log('Webhook received');
  Promise.all(req.body.events.map(handleEvent))
    .then((result) => {
      console.log('Event handled successfully');
      res.json(result);
    })
    .catch((error) => {
      console.error('Error in webhook handling:', error);
      res.status(500).end();
    });
});

// 薬名に基づきGASから薬の詳細情報を取得する
async function getDrugInfo(drugName) {
  const url = `https://script.google.com/macros/s/GASで取得したIDを入力/exec?query=${encodeURIComponent(drugName)}`;
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

// メイン処理: メッセージのイベントを処理
async function handleEvent(event) {
  try {
    console.log('Handling event:', event);

    if (event.type !== 'message' || event.message.type !== 'image') {
      console.log('Non-image message received');
      return Promise.resolve(null);
    }

    const imageURL = await getImageFromLine(event.message.id);
    console.log('Image URL fetched:', imageURL);

    const prediction = await classifyImage(imageURL);
    console.log('Prediction result:', prediction);

    // 予測結果を処理して、最も確率の高いクラスを取得
    const predictionData = await prediction.array();
    console.log('Prediction array:', predictionData);  // 配列形式の予測結果をログ出力

    const classIndex = predictionData[0].indexOf(Math.max(...predictionData[0]));  // 最も高い確率のクラスを取得
    const classLabels = ['ロキソプロフェンEMEC　60ｍｇ', 'レバミピド１００ｍｇ　ＥＭＥＣ'];  // Teachable Machineのクラスラベルに応じて修正
    const predictedClass = classLabels[classIndex];
    console.log('Predicted class:', predictedClass);  // 選ばれたクラスラベルをログ出力

    // GASから薬の詳細情報を取得
    const drugInfo = await getDrugInfo(predictedClass);
    console.log('Drug Info fetched:', drugInfo);

    // 詳細情報を返信メッセージに整形
    const replyMessage = `薬名: ${drugInfo.薬名}\n詳細: ${drugInfo.詳細}\n禁忌情報: ${drugInfo.禁忌情報 || 'なし'}\n出荷情報: ${drugInfo.出荷情報 || '通常出荷'}`;

    // 判定結果をLINEに返信
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: replyMessage
    });
  } catch (error) {
    console.error('Error in handleEvent:', error);
    throw new Error('Event handling failed');
  }
}

// LINEから画像を取得
async function getImageFromLine(messageId) {
  const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`;
  const headers = {
    Authorization: `Bearer ${config.channelAccessToken}`,
  };
  const response = await fetch(url, { headers });

  if (!response.ok) {
    throw new Error(`Failed to fetch image: ${response.statusText}`);
  }

  const buffer = await response.buffer();
  return buffer;
}

// 画像を判定（Teachable Machineモデルを使用）
async function classifyImage(imageBuffer) {
  const imageTensor = tf.node.decodeImage(imageBuffer);
  const resizedImageTensor = tf.image.resizeBilinear(imageTensor, [224, 224]);
  const model = await tf.loadLayersModel('file://model/model.json');
  const batchedImage = resizedImageTensor.expandDims(0);
  const prediction = model.predict(batchedImage);

  return prediction;
}

app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

```
</details>

::: note warn
**注意点**
- LINE Developersで獲得したチャンネルアクセストークン、チャネルシークレットは個別入力
- GASでデプロイした時に獲得したURLを個別入力
- Teachable Machineでダウンロードしたモデルの保存先を個別入力 
:::

###  **Webhook URLを設定してLINE Botをテスト**
   1. `ngrok` をインストールしていない場合、[ngrok](https://ngrok.com/)をインストールし、`ngrok http 3000` でローカルサーバーを公開します
   2. `ngrok`で生成されたURLをLINE DeveloperのWebhook設定に追加します
   3. LINEでテストメッセージを送信し、画像判定が正しく動作することを確認します


## 使ってもらった感想
自店の調剤事務に試しに使ってもらいました！

::: note
- 難しいかもしれませんが、全種類データに入れてほしい
- 毎回卸に電話をしなくていいのは本当に助かる！
- 写真を撮ると薬の効能がすぐに分かる機能は、医療従事者というよりは一般向け。家の中に眠ってる薬の効能が分かるのは便利

:::

色んな意見を頂きました。
![1000022668.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3839271/f7f79b5d-b819-bdb5-ee7f-bb8f51f67b6c.jpeg)
## 技術面での課題
- データベースの拡大：膨大な薬の情報をどうするか・・・!

- カード形式のメッセージ表示：テキスト表示だと見ずらいため、分かりやすい表示に変更する
- サーバーの問題：現在、ngrokをつけっぱなしじゃないと使えない
- 相互作用チェック機能：複数の薬が処方された際に、自動で判別
などなど

実用させるにはまだまだ沢山の課題があります

## 今後の展望：薬の監査システムへの発展
このプロトタイプは、薬の供給状況を効率的に確認するためのツールとして開発されましたが、将来的にはさらに**薬の監査システム**として発展させたいと考えています。

薬局の現場では、薬剤の監査作業に非常に時間と労力がかかります。現在の監査システムは、JANコードや重さで薬を判定する機器が一般的ですが、これに対し**画像認識技術を応用して、よりスマートに監査を行うシステム**を構築することが目標です。

**具体的には**
- **複数の薬剤を同時に認識**し、正確な監査結果を提供
- **薬剤の詳細情報や禁忌情報**の自動提供による監査の効率化

今回のシステムと合わせて利用することで、**薬剤師や事務員の負担を大幅に軽減**し、**調剤ミスの防止や時間短縮**に寄与することが出来たらと思っています。