現役、理学療法士が姿勢の画像を送るとその姿勢について教えてくれるLINE botを作った

#あなたの姿勢、本当に正しいですか？
どうもです。筆者は、都内の整形外科病院に勤務している、**現役、理学療法士**です。
理学療法士は、一言で言うと**「リハビリ」**の人って感じですが、**人の動きをみる専門家**でもあります。

日々、リハビリ業務を行う中で、首の痛みや、腰の痛みを訴えている患者さんに**姿勢指導**を行うことがあるのですが、その時に初めて自分の姿勢が誤っていることに気がつく方が多くいます。

　**「え、私これがいい姿勢だと思っていました。」**

**　「これだとなんか変に感じるんですが、これが正しい姿勢なんですね」**

一般の方の認識と、専門家からの評価が**乖離しているケース**が多くあるなーと日々感じていました。
テクノロジーを使って簡単にこの基準を整備できれば、姿勢から起こる疼痛を防ぐことが可能なのではないのかなーと感じ、開発しました。
このボットが正しい姿勢が気になる人への手助けとなればと思います。

注意）このボットはあくまで参考程度に作成したもので、正確な診断ツールではありません！　
　　　最終的な診断については、おかかりいただいた先生にお伺いください。


[ちなみに、正しい姿勢は、当院の医療コラムの記事を拝読ください！！！！！](https://clinic.adachikeiyu.com/924)

##実装内容
・LINE上で姿勢の画像を送ると、その姿勢について、正常姿勢 or フラットバック or スウェイバックのいずれかを判定してくれる
・認識した結果をもとに、その姿勢が及ぼす影響を記載した画像を送付してくれる。

https://twitter.com/yusei2629/status/1421395646037118979

##概念図
![姿勢LINE 2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/7e87e3ac-1b03-629b-c911-83857b8e5284.jpeg)
##作成方法
**1.開発準備**
　
**2.LINE-botの作成**

**3.Teachable Machineによる画像認識AIの作成**

**4.プロジェクトの作成・実行に必要なライブラリの追加**

**5.ｐosture.jsのコード**

**6.チャンネルアクセストークンとチャンネルシークレットの入力**

**7.ngrok でトンネリングし、LINE-botを動かす。**







##1.開発準備
**VScode**・**Node.js**・**npm**のインストール

手順が不明な場合は下記の記事をご確認いただき、インストールしてください！
[MacにNode.js,npmのインストール方法](https://qiita.com/Teru_3/items/cc8a4968eff131d00784)

ちなみに、筆者の環境は以下になります。 (2021/8/14時点)

`・Node.js v14.17.0`
`・npm v7.19.1`
`・macOS Big Sur 11.4`

##2.LINE-botの作成##
１）　LINE Developersにアクセスし、LINEアカウントでログイン
２）　プロバイダー作成
３）　新規チャンネルの作成
４）　**チャネルアクセストークン**と**チャネルシークレット**の取得
１）〜４）までは下記を参考に進みました。
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d#1-bot%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)

##3.Teachable Machineによる画像認識AIの作成
[Teachable　Machine](https://teachablemachine.withgoogle.com/)
Google謹製の「機械学習の独自モデルが作成できるWebアプリケーション」です。

独自の機械学習モデルを作る上では今のところ最も簡単な方法。ボタンをクリックするだけ
学習データセットもその場でWebカメラ・マイクから手軽に収集して利用できる

現時点では**画像分類**・**音声分類**・**姿勢推定**をサポート
作成したモデルはml5.jsで利用できるほか、通常のNode.jsやRaspberry Pi上などでも利用可能
厳密には転移学習という手法により、既存のモデルを再トレーニングすることによって独自モデルを構築している

![スクリーンショット 2021-08-13 23.05.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/a95bbcaa-301c-b845-328e-841eac2bbc97.png)
今回はTeachable Machineの　**ポーズプロジェクト**を使用しました。
![スクリーンショット 2021-08-14 4.34.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/057c4ba6-b335-f102-8553-928bf8501c0d.png)

今回、筆者はWEBカメラを使用し、[3種類の姿勢](https://rehatora.net/%E8%84%8A%E6%9F%B1%E3%81%AE%E5%BC%AF%E6%9B%B2%E3%81%AB%E3%82%88%E3%82%8B%E5%A7%BF%E5%8B%A2%E3%81%AE%E5%88%86%E9%A1%9E%EF%BC%88%E3%82%B1%E3%83%B3%E3%83%80%E3%83%AB%E3%81%AE%E5%88%86%E9%A1%9E%EF%BC%89/)　(正常姿勢、スウェイバック、フラットバック)
を撮影し学習させました。
![スクリーンショット 2021-08-14 4.56.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/5c4372a1-ffe3-6e92-a099-7ea0a920893d.png)

今回、Teachable　Machineの利用方法について参考にさせて頂いたのは、[こちら](https://qiita.com/misa_m/items/d8a20d153c8ee7daf0c1)の記事です。

オリジナルモデルを作成後は、モデルをエクスポートし、クラウド上にアップロードしましょう！
**今回のモデルのURLは[こちら](https://teachablemachine.withgoogle.com/models/wI7bObtCw/)**
(プレビュー画面でお持ちの画像で試せるので試してみてください！)

##4.プロジェクトの作成・実行に必要なライブラリの追加##

**プロジェクトの作成**

Node.jsのプロジェクトは`package.json`があるディレクトリが起点となります。
ターミナルを開き、`npm init`コマンドで`package.json`を作成します。

```
$ mkdir Posture-app
$ cd Posture-app
$ npm init -y
```
作ったフォルダをVS codeで開きます。

**ライブラリの追加**
VS code　でターミナルを開き、今回の挙動に必要なパッケージをインストールします。

`①LINE-botを動かすために必要なライブラリ`

```
$ npm i @line/bot-sdk express
```
`②TeachableMachineを動かすために必要なライブラリ`

```
$ npm i @teachablemachine/pose　@tensorflow/tfjs canvas jsdom　jimp
```

**package.json**
![スクリーンショット 2021-08-14 6.48.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/4837ae56-6de8-aa9a-468e-b6572d5937ed.png)

##5.posture.jsのコード
`Posture-app` フォルダの中に、`posture.js`　ファイルを作成します。
その後、以下をコピー&ペーストしてください。

```javascript:posture.js

"use strict"; 

const { JSDOM } = require("jsdom");
var dom = new JSDOM("");
global.document = dom.window.document;
global.HTMLVideoElement = dom.window.HTMLVideoElement;
const canvas = require("canvas");
global.fetch = require("node-fetch");
const tmPose = require("@teachablemachine/pose");

const express = require("express");
const line = require("@line/bot-sdk");
const fs = require("fs");
const Jimp = require("jimp");

const PORT = process.env.PORT || 3000;

const config = {
  channelSecret: "シークレット",
  channelAccessToken: "アクセストークン",
};

// Teachable Machine
let model;

// https://teachablemachine.withgoogle.com/
// ここでエクスポート、クラウドにモデルをアップロードした後に取得できる
const URL = "https://teachablemachine.withgoogle.com/models/wI7bObtCw/";

// ########################################
//  Teachable Machineを使って画像分類をする部分
// ########################################~~

// 初期化が時間かかるので、node立ち上げ時に行うようにする
async function initTeachableMachine() {
  console.log("モデルを読み込んでいます...");
  const modelURL = URL + "model.json";
  const metadataURL = URL + "metadata.json";
  // モデルデータのロード
  model = await tmPose.load(modelURL, metadataURL);
	console.log("モデルの読み込みが完了しました。");

  // クラスのリストを取得
  // const classes = model.getClassLabels();
  // console.log(classes);
}
initTeachableMachine();

async function predict(imgPath) {
	// 画像の幅、高さを取得する
	const jimpImage = await Jimp.read(imgPath);
	console.log(jimpImage.bitmap.width, jimpImage.bitmap.height);

  // canvasに画像をロードする
	const imageCanvas = canvas.createCanvas(jimpImage.bitmap.width, jimpImage.bitmap.height);
	const ctx = imageCanvas.getContext('2d');
  const image = await canvas.loadImage(imgPath);
	ctx.drawImage(image, 0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height);

  // 判定する
	const { posenetOutput } = await model.estimatePose(imageCanvas);
  const predictions = await model.predict(posenetOutput);

  // 一番近いもの順でソート
  predictions.sort((a, b) => {
    return b.probability - a.probability;
  });

  return predictions;
}

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
  // 受信したWebhookが「画像以外」であればnullを返すことで無視します
  if (event.message.type === "image") {
    console.log("画像が送られてきた");

    // 画像を保存
    const downloadPath = "./01.png"; //左記の名前で使用中のフォルダ内に送信された画像が保存される
    const getContent = await downloadContent(event.message.id, downloadPath);

    const result = await predict(downloadPath);

    // AIメーカーAPIの結果から、返信するメッセージを組み立てる
    let text = "";
    let name = "";
    name = result[0].className;
    // 判定結果をテキストに代入
    text = "あなたは『" + name + "』に最も似ています！";
    // これまでの結果を確認するためにコンソールに表示
    console.log(result);
    console.log(name);
    console.log(text);
    // 判定結果に応じた画像を送信
    if (name === "健常者") {
      return client.replyMessage(event.replyToken, [
        {
          type: "text",
          text: text,
        },
        {
          type: "image",
          originalContentUrl: "https://ar-ex.jp/uploads/ckeditor4/pictures/565645433405/content_%E5%A7%BF%E5%8B%A2%E2%85%A0.jpg",
          previewImageUrl: "https://ar-ex.jp/uploads/ckeditor4/pictures/565645433405/content_%E5%A7%BF%E5%8B%A2%E2%85%A0.jpg",
        },
      ]);
    } else if (name === "スウェイバック") {
      return client.replyMessage(event.replyToken, [
        {
          type: "text",
          text: text,
        },
        {
          type: "image",
          originalContentUrl: "https://i.pinimg.com/564x/e5/2a/b8/e52ab80290de494a894fabe1c7c796e6.jpg",
          previewImageUrl: "https://i.pinimg.com/564x/e5/2a/b8/e52ab80290de494a894fabe1c7c796e6.jpg",
        },
      ]);
    } else if (name === "フラットバック") {
      return client.replyMessage(event.replyToken, [
        {
          type: "text",
          text: text,
        },
        {
          type: "image",
          originalContentUrl: "https://i.pinimg.com/564x/36/0a/01/360a012f91ef0e37f45ea871c9b6f4d2.jpg",
          previewImageUrl: "https://i.pinimg.com/564x/36/0a/01/360a012f91ef0e37f45ea871c9b6f4d2.jpg",
        },
      ]);
    }
  }
  // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
  if (event.message.type === "text") {
    return client.replyMessage(event.replyToken, {
      type: "text",
      text: event.message.text, // ← ここに入れた言葉が実際に返信されます
    });
  }
}

// ########################################
//          LINEで送られた画像を保存する部分
// ########################################

function downloadContent(messageId, downloadPath) {
  const data = [];
  return client.getMessageContent(messageId).then(
    (stream) =>
      new Promise((resolve, reject) => {
        const writable = fs.createWriteStream(downloadPath);
        stream.on("data", (chunk) => data.push(Buffer.from(chunk)));
        stream.pipe(writable);
        stream.on("end", () => resolve(Buffer.concat(data)));
        stream.on("error", reject);
      })
  );
}

// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します
// これはMessaging APIとは関係のない確認用のものです
app.get("/", (req, res) => res.send("<h1>Hello LINE BOT! (HTTP GET)</h1>"));

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post("/webhook", line.middleware(config), (req, res) => {
  // Webhookの中身を確認用にターミナルに表示します
  console.log(req.body.events);

  // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用
  // 削除しても問題ありません
  if (req.body.events.length == 0) {
    res.send("Hello LINE BOT! (HTTP POST)"); // LINEサーバーに返答します
    console.log("検証イベントを受信しました！"); // ターミナルに表示します
    return; // これより下は実行されません
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

```
##6.チャンネルアクセストークンとチャンネルシークレットの入力
先ほどの「LINE developers」の画面で``Channel Secret``と``Channel Access Token``を確認し、上記のソースコードに入力します。
![スクリーンショット 2021-08-14 6.57.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/e877fa0c-57a2-927c-74c3-938d2a5ff29c.png)


![スクリーンショット 2021-08-14 6.58.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/789f69d1-2ef8-bda9-b8e1-6a3ee9035cbb.png)

##7.ngrok でトンネリングし、LINE-botを動かす。
[ngrok](https://ngrok.com/)
npm経由でngrokをインストールできます。

```
$ npm i -g ngrok
```
これで　ngrokが使えるようになりましたので、早速動かしていきます。

まず、**アプリケーションを起動**しましょう。

```
$ node posture.js
```
正常通り動けば下記のように表示されます。
![スクリーンショット 2021-08-14 7.21.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/36b70f92-871f-6ad6-f8e7-e4ae9e2822a2.png)


その後、もう一つ、ターミナルを開き、次にトンネリングサーバーを起動しましょう。

 `ngrok http ポート名`と指定します。

今回はNode.jsアプリケーションを3000番ポートで利用するので3000を指定しましょう.

```
$ ngrok http 3000
```

`※このアプリケーションと、サーバーの起動の順番を間違うとエラーが生じます。
アプリケーションが起動したのを確認したのちに、サーバーを起動しましょう！！`

正常通り動けば下記のように表示されます。
![スクリーンショット 2021-08-14 7.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/713a6753-5f50-63f0-dca7-e2e37ffde554.png)
そして赤枠がWebhook URLになるので、このURLでLINE developers画面のWebhook URLを更新します。
**※末尾に　/webhook　　をつけることを忘れずに。**
![スクリーンショット 2021-08-14 18.11.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/b3f6f99b-7e8e-e97a-b99f-817dbec73c7c.png)


##QRコード
完成した姿勢判定botはこちらです。
![スクリーンショット 2021-08-14 14.37.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1831331/346d27e2-941e-fc73-a488-7b7a4d49609e.png)