デイサービスでリハビリ器具を覚えてトレーニングを判断するAIツールの作成
はじめに
デイサービスにおいて、新しく入ったスタッフがリハビリ器具を適切に使用するには、器具の使用方法やトレーニングの目的を理解することが重要です。しかし、すべてのスタッフが理学療法士(PT)や作業療法士(OT)のような専門知識を持っているわけではなく、器具の使い方をすぐに覚えるのは簡単ではありません。そこで、Teachable Machineを活用し、リハビリ器具を識別して適切なトレーニングを案内するツールを作成しました。このツールを使えば、専門職でなくても利用者に合ったトレーニングのサポートが容易にできないか試案しました。
このシステムは、実際のリハビリ器具を認識し、その器具に適したトレーニング内容や注意点を表示します。これにより、デイサービス現場での業務効率が向上し、新しいスタッフでも安全にトレーニングをサポートすることができます。
リハビリで使用する道具を見て、どんなトレーニングをすればいいか教えてくれます。
課題
リハビリ器具の適切な使用方法を知らないと、利用者に適切なトレーニングを提供することが難しくなります。特に、新しいスタッフや経験の少ないスタッフにとって、どの器具がどのトレーニングに対応しているのかを覚えることは負担です。専門知識を持たないスタッフが誤った使用方法でトレーニングを指導してしまうリスクも高く、結果として利用者の体に負担をかけてしまうことも懸念されています。
使用したツール
今回のプロトタイプを作成するにあたり、以下のツールを使用しました。
- Teachable Machine: Googleが提供する機械学習ツール。リハビリ器具の画像をトレーニングさせることで、簡単に識別ができるようになっています。AIモデルを使い、器具を判別し、その結果を返す仕組みです。
- CodePen: プロトタイプのウェブアプリを素早く構築するために利用しました。HTMLとJavaScriptを使ってリアルタイムで動作を確認でき、器具の認識結果をその場で表示できるようにしました。
実際に使用したリハビリ器具
今回のツールでは、4つのリハビリ器具を識別できるようにTeachable Machineでトレーニングを行いました。各器具について、100枚以上の画像を撮影し、モデルをトレーニングしました。使用した器具は以下の通りです。
Teachable Machineの画像プロジェクトを選択し、4つのクラスに分類しました。
実装の流れ
1. Teachable Machineでのモデル作成
Teachable Machineを使って、各リハビリ器具の画像を収集し、機械学習モデルをトレーニングしました。ステップは以下の通りです。
- データ収集: 各器具ごとに100枚以上の画像を収集し、それぞれの器具を分類しました。写真は異なる角度やライティング条件下で撮影し、モデルが正確に判別できるようデータを多様化させました。
- トレーニング: Teachable Machineで各器具のクラスに応じたモデルを作成し、識別精度を向上させました。
- モデルのエクスポート: 完成したモデルをエクスポートし、JavaScriptで読み込む形にしました。
2. CodePenでのプロトタイプ作成
CodePenを利用し、HTMLとJavaScriptを組み合わせて、リハビリ器具を識別するウェブアプリを構築しました。CodePenの便利な点は、ブラウザ上で即座に実装の動作を確認できるところです。器具を映すと、リアルタイムでトレーニングに適した器具名が表示される仕組みになっています。これにより、複雑な設定を必要とせず、どこでも手軽にプロトタイプのテストが可能です。
トレーニングメニューのHTMLコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>トレーニングメニュー</title>
</head>
<body>
<h1>トレーニングメニュー</h1>
<button type="button" onclick="init()">Webcamで開始</button>
<div id="webcam-container"></div>
<div id="label-container"></div>
<div id="exercise-container">
<!-- ここにトレーニング内容が表示されます -->
</div>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@latest/dist/teachablemachine-image.min.js"></script>
<script src="app.js"></script>
</body>
</html>
トレーニングメニューのJavaScriptコード
// Teachable MachineのモデルURL
const URL = "https://teachablemachine.withgoogle.com/models/q68AstIGp/";
let model, webcam, labelContainer, maxPredictions;
// モデルとWebcamを初期化する関数
async function init() {
const modelURL = URL + "model.json";
const metadataURL = URL + "metadata.json";
// モデルとメタデータをロード
model = await tmImage.load(modelURL, metadataURL);
maxPredictions = model.getTotalClasses();
// Webcamを設定
const flip = true;
webcam = new tmImage.Webcam(200, 200, flip); // 幅, 高さ, 反転
await webcam.setup(); // Webcamアクセス許可
await webcam.play();
window.requestAnimationFrame(loop);
// WebcamをDOMに追加
document.getElementById("webcam-container").appendChild(webcam.canvas);
labelContainer = document.getElementById("label-container");
for (let i = 0; i < maxPredictions; i++) {
labelContainer.appendChild(document.createElement("div"));
}
}
// Webcamループ
async function loop() {
webcam.update(); // Webcamフレームを更新
await predict(); // 予測
window.requestAnimationFrame(loop);
}
// 予測関数
async function predict() {
// 予測結果を取得
const prediction = await model.predict(webcam.canvas);
let highestProbability = 0;
let highestClass = "";
// 最も確率が高いクラスを選択
for (let i = 0; i < maxPredictions; i++) {
const classPrediction = prediction[i].className + ": " + prediction[i].probability.toFixed(2);
labelContainer.childNodes[i].innerHTML = classPrediction;
if (prediction[i].probability > highestProbability) {
highestProbability = prediction[i].probability;
highestClass = prediction[i].className;
}
}
console.log("認識されたクラス:", highestClass); // クラス名をコンソールに表示
// クラスに応じてトレーニング内容を表示
displayExercise(highestClass);
}
// トレーニング内容を表示する関数
function displayExercise(className) {
const exerciseContainer = document.getElementById("exercise-container");
let exerciseDescription = '';
switch(className) {
case "棒(体操用)":
exerciseDescription = "<h2>クラス1: 棒(体操用)</h2><p>棒の挙上10回、左右回旋10回</p>";
break;
case "エクササイズボール":
exerciseDescription = "<h2>クラス2: エクササイズボール</h2><p>両太ももで3秒挟んで緩めるを10回繰り返す</p>";
break;
case "セラバンド":
exerciseDescription = "<h2>クラス3: セラバンド</h2><p>両ひざ下にセラバンドを付け、平行棒につかまりながら5往復横歩き</p>";
break;
case "バランスマット":
exerciseDescription = "<h2>クラス4: バランスマット</h2><p>ゆっくり腿上げを各足10回ずつ</p>";
break;
default:
exerciseDescription = "<h2>トレーニングメニュー</h2><p>認識されたクラスに応じてトレーニング内容が表示されます。</p>";
}
exerciseContainer.innerHTML = exerciseDescription;
}
導入のメリット
このツールを導入することで、理学療法士や作業療法士が不在の時でも、スタッフが利用者に適切な器具を提供できるようになります。例えば、膝に不安がある利用者にエクササイズボールを使う際は、運動強度を調整するなどの注意事項を提示することで、安全にトレーニングを進めることができます。
また、利用者の体調や運動能力に応じたトレーニングの提案ができるため、個別対応が求められるデイサービス現場においても大いに役立ちます。これにより、利用者一人ひとりの状態に合わせた安全で効果的なリハビリを提供することが可能となります。
今後の展望として
今後は、リハビリ器具の識別に加えて、利用者の疾患や体調に応じた注意事項を表示できるようにしたいと考えています。例えば、膝や腰に痛みがある利用者には特定の器具を避けるようにするなど、個別の対応を支援する機能です。また、今後はリハビリ用のマシンも加えて、幅広いトレーニングに対応したツールへと発展させていきます。
さらに、利用者の運動履歴やトレーニングデータを蓄積することで、個別に最適化されたリハビリプランを提案できるような仕組みも構築する予定です。これにより、さらに高度なリハビリサポートが可能となり、利用者の状態に合わせた最適なトレーニングプランの提供が実現します。
直属の上司からは、自宅でご利用者が自主トレーニングに使えるといいね。とアドバイスを頂戴しました。身近にあるもの、タオルやペットボトル等の写真を認識して、できる運動のアドバイスができれば、機能として活用できる幅が広がるのではないかと思います。
まとめ
今回開発したツールは、リハビリ器具の識別とトレーニング内容の判別を容易にし、専門職以外のスタッフでも利用者に安全で効果的なトレーニングを提供できるようにするものです。今後も改善を重ね、デイサービスでのリハビリをサポートするツールとして発展させていきたいと考えています。今回は、各用具に対して、実施できるトレーニングメニュー数は少なかったですが、今後は、ADLや身体機能のレベルに合わせて、階層に合わせたメニューが提案できるようにするなど、工夫を加えていきたいと思います。実施時の注意点や禁忌についても記載するのも面白いかもしれません。現場の専門職と相談を実施し、ブラッシュアップしたいです。
参考文献
編集後記
本当ならば、立位や座位の姿勢判別をしたいと思いチャレンジしましたが、動きがあるものを機会に学習させることはとても難しいと感じました。
座位・立位で3パターンずつ学習させました。それぞれ、正しい姿勢、猫背、後ろ重心をポーズプロジェクトで学習しましたが、正しい姿勢や後ろ重心の姿勢になりますが、猫背の判定が出てしまいました。それぞれのクラスで100枚以上の画像を撮影しましたが、正しい判別ができませんでした。原因として、動作の過程で、似た動きが入ってしまったことが原因ではないかと思っております。撮影方法、学習方法を再度、検討してリトライしてみたいと思います。
修行は続く.....
姿勢判断ツールプロトタイプ作成
姿勢判別ツールのプロトタイプを作成しました。
自宅の環境では設備が揃っていないため、Teachable Machineに学習して貰った環境での測定が実施できないため、精度に関しては不明です。変更した点は、複数の項目を判別するパラメータが多いと見にくくなるため、動きが正しくできているか、採点方式で判断してもらうようにしました。高齢者の方は、立ち上がってから座る時に、勢いよく座る傾向があるため、座位になる動作時、速度が過ぎる場合は減点するようにしております。点数によりコメントが出るようになっております。使用したHTML、CSS、JSは折りこみ記事にしてありますので、ご興味のある方はご覧下さい。
※アプリのイメージ画像です。
HTML コード
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Teachable Machine Pose Model - 評価版</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div>Teachable Machine Pose Model</div>
<button type="button" onclick="init()">Start</button>
<div>
<canvas id="canvas"></canvas>
</div>
<div id="label-container" style="display:none;"></div> <!-- 6つの項目は非表示 -->
<div id="score">評価: 0 / 100</div>
<div id="comments"></div>
<!-- TensorFlow and Teachable Machine Pose library -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.3.1/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/pose@0.8/dist/teachablemachine-pose.min.js"></script>
<!-- Link to your JavaScript file -->
<script src="app.js"></script>
</body>
</html>
CSS コード
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #4A90E2;
}
button {
padding: 10px 25px;
background-color: #4A90E2;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
margin-bottom: 20px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
button:hover {
background-color: #357ABD;
}
#canvas {
border: 2px solid #ddd;
border-radius: 10px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
#score {
font-size: 18px;
color: #333;
margin-bottom: 10px;
font-weight: bold;
}
#comments {
font-size: 16px;
color: #666;
padding: 10px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 300px;
}
JavaScript コード
const URL = "https://teachablemachine.withgoogle.com/models/2AhOEgcLq/";
let model, webcam, ctx, labelContainer, maxPredictions;
let sitting = false;
let standing = false;
let score = 100; // 100点満点の初期スコア
let startTime, endTime; // 速度を計測するためのタイマー
let correctMoves = 0;
let incorrectMoves = 0;
let comments = "";
async function init() {
const modelURL = URL + "model.json";
const metadataURL = URL + "metadata.json";
try {
model = await tmPose.load(modelURL, metadataURL);
maxPredictions = model.getTotalClasses();
// Setup webcam
const size = 200;
const flip = true;
webcam = new tmPose.Webcam(size, size, flip);
await webcam.setup();
await webcam.play();
window.requestAnimationFrame(loop);
const canvas = document.getElementById("canvas");
canvas.width = size;
canvas.height = size;
ctx = canvas.getContext("2d");
labelContainer = document.getElementById("label-container");
for (let i = 0; i < maxPredictions; i++) {
labelContainer.appendChild(document.createElement("div"));
}
} catch (error) {
console.error("Error initializing the model or webcam:", error);
}
}
async function loop() {
webcam.update();
await predict();
window.requestAnimationFrame(loop);
}
async function predict() {
const { pose, posenetOutput } = await model.estimatePose(webcam.canvas);
const prediction = await model.predict(posenetOutput);
let currentPose = "";
let confidenceScore = 0;
for (let i = 0; i < maxPredictions; i++) {
if (prediction[i].className === "立っている" && prediction[i].probability > 0.8) {
currentPose = "standing";
confidenceScore = prediction[i].probability;
} else if (prediction[i].className === "座っている" && prediction[i].probability > 0.8) {
currentPose = "sitting";
confidenceScore = prediction[i].probability;
}
}
if (currentPose === "standing" && !standing) {
standing = true;
sitting = false;
startTime = performance.now(); // 立ち上がった瞬間の時間を記録
if (confidenceScore > 0.9) {
correctMoves++;
} else {
incorrectMoves++;
score -= 5; // 姿勢が悪いときは減点
}
}
if (currentPose === "sitting" && standing) {
standing = false;
sitting = true;
endTime = performance.now(); // 座った瞬間の時間を記録
const duration = endTime - startTime; // 動作の速さを計測
if (duration < 1000) { // 1秒未満の座る動作が速すぎると判断
score -= 10; // 座る動作が速すぎる場合は減点
} else if (confidenceScore < 0.9) {
incorrectMoves++;
score -= 5; // 姿勢が悪いときは減点
}
}
// スコアとコメントを更新
if (score > 90) {
comments = "素晴らしい!動作が完璧です!";
} else if (score > 70) {
comments = "よくできました!";
} else if (score > 50) {
comments = "姿勢をもう少し意識しましょう。";
} else {
comments = "もう少し頑張りましょう。";
}
document.getElementById("score").innerText = `評価: ${score} / 100`;
document.getElementById("comments").innerText = comments;
drawPose(pose);
}
function drawPose(pose) {
if (webcam.canvas) {
ctx.drawImage(webcam.canvas, 0, 0);
if (pose) {
const minPartConfidence = 0.5;
tmPose.drawKeypoints(pose.keypoints, minPartConfidence, ctx);
tmPose.drawSkeleton(pose.keypoints, minPartConfidence, ctx);
}
}
}