「膝のレントゲンなんて持ってないっすよ。」
以前に書いた記事、『誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。』を様々な人に紹介したところ、「すごくいいと思うけど、膝のレントゲンなんて持ってないよ(笑)」と多くの方から生暖かい目で諭されました。
安心してください。想定の範囲内ですよ。
そこで、今回は膝の外表面写真からその膝が変形しているかどうか判定するbotを作りました。
挙動は以下の通りです。
精度は微妙だけど、外表面から変形の程度を判断するbotも完成。#protoout pic.twitter.com/S0kDG0FGS6
— 北城雅照@足立慶友整形外科 (@kutuyanomusuko) November 29, 2020
* 「変形性膝関節症って何?」という方は、僕が書いたこちらの記事をぜひお読みください。
変形性膝関節症とは:その治療法・進行予防について
注意1) 「変形性膝関節症って何?」という方は、僕が書いたこちらの記事をぜひお読みください。
変形性膝関節症とは:その治療法・進行予防について
注意2) このボットはあくまで啓蒙活動の一貫で作成したもので、正確な診断ツールではありません!最終的な診断については、おかかりいただいた先生にお伺いください。
システムの概要
開発環境について
今回はAIメーカーを利用して実装いたしました。
実装の内容については上記の記事『誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。』をご確認ください。
開発環境の準備
1) kneephotoというフォルダを作成
2) VSCodeで上記フォルダを開き、フォルダ内にkneephoto.jsを作成
3) VSCode内でターミナルを開き、フォルダをnpm管理できるように初期化
$ npm init -y
4) フォルダ内にラインボット用のパッケージをnpmでインストール
$ npm i @teachablemachine/image @tensorflow/tfjs canvas jsdom
判定の方法の医学的背景について
今回、判定の方法に用いた医学的根拠について詳述します。興味のある方はお読みください。
「早く使いたい!」という方は、一番下のQBコードまで進んでください。
1)膝の痛みを主訴に受診された患者様の膝外表面写真を、膝を中心に足関節が入るように撮影
2)上記患者様の膝のレントゲン写真を、経験のある整形外科(つまり私)が、KL分類をもとに分類
*ちなみにKL分類は下記の分類です。
3)レントゲンの分類をもとに、外表面写真を正常群22例(グレード0とグレード1)、変形性膝関節症群35例(グレード2,グレード3,グレード4)の2群に分類
4)AIメーカーに正常膝と変形性関節症膝の2つのラベルを用意し、そこに画像をアップロードして学習
kneephoto.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 tmImage = require('@teachablemachine/image');
const express = require('express');
const line = require('@line/bot-sdk');
const fs = require('fs');
const PORT = process.env.PORT || 3000;
const config = {
channelSecret: 'チャンネルシークレット',
channelAccessToken: 'アクセストークン',
};
// Teachable Machine
let model;
// https://teachablemachine.withgoogle.com/
// teachablemachineから取得したURLを記載
const URL = 'https://teachablemachine.withgoogle.com/models/970E5c9Me/';
// ########################################
// Teachable Machineを使って画像分類をする部分
// ########################################
// 初期化が時間かかるので、node立ち上げ時に行うようにする
async function initTeachableMachine() {
const modelURL = URL + 'model.json';
const metadataURL = URL + 'metadata.json';
// モデルデータのロード
model = await tmImage.load(modelURL, metadataURL);
}
initTeachableMachine();
async function predict(imgPath) {
// canvasに画像をロードする
const image = await canvas.loadImage(imgPath);
// 判定する
const predictions = await model.predict(image);
// 一番近いもの順でソート
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);
console.log(result);
// Teachable Machineの結果から、返信するメッセージを組み立てる
let text = '';
for (const { className, probability } of Object.values(result)) {
text += `${className}の確率は${(probability * 100).toFixed()}%\n`;
}
text += 'です。';
console.log(text);
return client.replyMessage(event.replyToken, {
type: 'text',
text: text
});
}
// 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
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サーバーを実行中です…`);
#完成したラインbot
完成したラインbotのQRコードがこちらです。
使用上注意点と今後の課題
1)まだまだ精度が低いので写真を増やしたり、撮り方を統一したりなどの調整を行なっていきます。
2)膝を中心に足首が入るように素足で写真を撮ってください。
3)繰り返しになりますが、診断ツールではありません。啓蒙ツールです。ご利用は計画的にお願いします。
その他の記事
1)誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。