React Native、TensorFlow.js、ExpoでAI姿勢矫正アプリを作る
モバイルAIフィットネスの構築手順ガイド。React Native、TensorFlow.js、リアルタイム姿勢推定で運動姿勢を分析・矫正。デバイス上で処理しプライバシーも保護。
このページの内容
誰もが経験ことがあるでしょう:家でワークアウトビデオを見ながら、自分のフォームが正しいかどうか不思議に思う。このスクワット中、背中はまっすぐですか?膝が内側に入っていませんか?不正確な運動フォームは、ワークアウトの効果を低下させるだけでなく、怪我の主な原因でもあります。パーソナルトレーナーさんは高くつきフィットネスアプリはほとんどが単なるビデオプレーヤーです。
お使いのスマートフォンがパーソナルトレーナーになれるとしたらどうでしょうか?
このチュートリアルでは、まさにそれを構築します:スマートフォンのカメラとオンデバイスマシンラーニングを使用して、リアルタイムに運動フォームのフィードバックを提供するReact Nativeアプリケーションです。TensorFlow.jsの力を活用して、最先端の姿勢推定モデルであるMoveNetをデバイス上で直接実行します。このアプローチは高速でオフラインでも動作し、最も重要的是に完全にプライベートです—カメラフィードがサーバーに送信されることはありません。
構築するもの: 次のようなシンプルなワークアウトアシスタントを作成します:
- Expo Cameraを使用して前面カメラにアクセスします。
- TensorFlow.jsとMoveNetポーズ検出モデルでリアルタイムにビデオストリームを分析します。
- カメラフィードの上に検出されたボディキーポイントと骨格を描画します。
- 関節角度を計算して、スクワットが正しく実行されているかどうかを判断します。
- 「もっと深く!」や「良いスクワット!」などのシンプルで実行可能なフィードバックを表示します。
このプロジェクトは、AI搭載のヘルステクノロジーの世界に参入する完璧なエントリーポイントであり、コンピュータビジョンとモバイル開発を実用的でインパクトのある方法で組み合わせています。
問題の理解
主な課題は、運動する人のビジュアルストリームを実行可能なフィードバックに変換することです。これはいくつかの技術的な課題に分解されます:
- リアルタイム分析:即座のフィードバックを遅延なく提供するために、ビデオフレームを十分に素早く処理する必要があります。
- 正確な姿勢推定:システムは主要なボディ関節(肩、肘、腰、ひざなど)の位置を正確に特定する必要があります。
- 生体力学的ロジック:関節位置を取得したら、フォームを評価するために生体力学に基づいたルールを適用する必要があります。たとえば、スクワットでは深度を決定するためにひざの角度を測定する必要があります。
- デバイス上のパフォーマンス:電話でディープラーニングモデルを実行すると、リソース集約的になる可能性があります。スムーズなユーザーエクスペリエンスを保証するために、効率的なモデルと最適化されたコードが必要です。
私たちのソリューションは、スマートフォンのようなデバイスでの速度と精度のために最適化されたモデルであるMoveNetを使用し、リアルタイムアプリケーションに最適です。
前提条件
このチュートリアルに従うには、以下が必要です:
- Node.js(LTSバージョン推奨)。
- パッケージ管理用のYarnまたはnpm。
- テスト用のiOSまたはAndroidスマートフォン上のExpo Goアプリ。
- カメラ機能をテストするには物理デバイスが必要です。iOS/Androidシミュレーターでは動作しません。
- ReactとReactフック(
useState、useEffect)の基本的な知識。
ステップ1:プロジェクトのセットアップ
まず、新しいExpoプロジェクトを作成し、必要な依存関係をインストールしましょう。
何をするか
空のExpoアプリを初期化し、カメラ、TensorFlow.js、GPUアクセラレーション用のWebGL、およびフィードバック描画用の2Dグラフィックスライブラリ用のライブラリを追加します。
実装
ターミナルを開き、次のコマンドを実行します:
# 新しいExpoプロジェクトを作成
npx create-expo-app ai-workout-trainer
cd ai-workout-trainer
# TensorFlow.jsとその依存関係をインストール
npm install @tensorflow/tfjs @tensorflow-models/pose-detection @react-native-async-storage/async-storage
# カメラ、WebGL(GPUサポート用)、ファイルシステム用のExpoモジュールをインストール
npm install expo-camera expo-gl expo-gl-cpp expo-file-system
# 描画用のキャンバスライブラリをインストール。React Native Skiaを使用します。
npm install @shopify/react-native-skia
動作の仕組み
@tensorflow/tfjs:コアTensorFlow.jsライブラリ。@tensorflow-models/pose-detection:MoveNetを含む事前学習済みモデルを提供します。expo-camera:デバイスのカメラフィードにアクセスして表示できます。expo-glとexpo-gl-cpp:これらはWebGLを有効にし、TensorFlow.jsが電話のGPUを使用してモデルの推論を大幅に高速化できるようにします。@shopify/react-native-skia:キャンバスオーバーレイに骨格とフィードバックを描画するために使用する強力な2Dグラフィックスライブラリ。
ステップ2:カメラとTensorFlow.jsの統合
次に、カメラを起動してAIモデルを読み込みましょう。TensorFlow.jsのセットアップを管理するカスタムフックを作成することから始めます。
何をするか
カメラコンポーネントをセットアップし、ユーザーの許可を求め、TensorFlow.jsを初期化し、MoveNetモデルを読み込む再利用可能なフックを作成します。これにより、メインコンポーネントがクリーンに保たれます。
実装
新しいファイルsrc/useTensorFlow.jsを作成します:
// src/useTensorFlow.js
import { useEffect, useState } from 'react';
import * as tf from '@tensorflow/tfjs';
import '@tensorflow/tfjs-react-native';
import * as poseDetection from '@tensorflow-models/pose-detection';
export const useTensorFlow = () => {
const [isReady, setIsReady] = useState(false);
const [model, setModel] = useState(null);
useEffect(() => {
const setup = async () => {
// 1. TensorFlow.jsの準備ができるまで待つ
await tf.ready();
// 2. MoveNet{detector}を作成
const detectorConfig = {
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING,
};
const detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
detectorConfig
);
setModel(detector);
// 3. 準備完了フラグを設定
setIsReady(true);
console.log('TF and model ready!');
};
setup();
}, []);
return { model, isReady };
};
次に、App.jsを更新してこのフックを使用し、カメラを表示します。
// App.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Dimensions } from 'react-native';
import { Camera, CameraType } from 'expo-camera';
import { useTensorFlow } from './src/useTensorFlow';
const { width, height } = Dimensions.get('window');
export default function App() {
const [cameraPermission, setCameraPermission] = useState(null);
const { model, isReady } = useTensorFlow();
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setCameraPermission(status === 'granted');
})();
}, []);
if (cameraPermission === null) {
return <View />;
}
if (cameraPermission === false) {
return <Text>No access to camera</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.loadingText}>
{isReady ? 'Ready!' : 'Loading Model...'}
</Text>
<Camera
style={styles.camera}
type={CameraType.front}
// 後ほどここにフレーム処理ロジックを追加します
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
camera: {
width: width,
height: height,
},
loadingText: {
position: 'absolute',
top: 50,
color: 'white',
fontSize: 18,
zIndex: 10,
},
});
動作の仕組み
useTensorFlowフック:モデル読み込みロジックをカプセル化します。tf.ready()を呼び出してTF.jsバックエンドを初期化し、次にposeDetection.createDetectorを呼び出して事前学習済みMoveNetモデルを読み込みます。リアルタイムモバイル使用に最適な最速バージョンであるため、SINGLEPOSE_LIGHTNINGを選択しました。App.js:カメラの使用許可をCamera.requestCameraPermissionsAsync()でリクエストします。カスタムフックがモデルの準備完了を報告するまで読み込みメッセージを表示します。最後に、フルスクリーンのCameraコンポーネントをレンダリングします。
今すぐnpx expo startでアプリを実行し、電話で開きます。「Loading Model...」メッセージが表示され、次に「Ready!」と前面カメラフィードが表示されます。
ステップ3:リアルタイム姿勢推定
これが魔法が起こる瞬間です!カメラからフレームをキャプチャし、モデルに送信して、姿勢キーポイントを取得する必要があります。expo-cameraは直接的なフレームバイフレームのストリームを提供しません。このチュートリアルでは、ループ関数内でtakePictureAsyncを使用した巧妙な回避策を使用します。
**注意:**本番アプリでは、react-native-vision-cameraとそのフレームプロセッサAPIがよりパフォーマンスの高い選択です。これは、画像を保存するオーバーヘッドを回避ためです。
何をするか
カメラから連続して画像をキャプチャし、TensorFlow.jsが期待するtensor形式に変換し、モデルを実行し、検出されたポーズをコンポーネントの状態に保存する関数を作成します。
実装
ポーズ検出ロジックでApp.jsを更新します:
// App.js (追加部分)
import { cameraWithTensors } from '@tensorflow/tfjs-react-native';
import * as FileSystem from 'expo-file-system';
// CameraをcameraWithTensors HOCでラップ
const TensorCamera = cameraWithTensors(Camera);
// ... Appコンポーネント内
const [poses, setPoses] = useState([]);
const cameraRef = React.useRef(null);
// この関数はカメラからの各フレームに対して呼び出されます
const handleCameraStream = (images) => {
const loop = async () => {
const nextImageTensor = images.next().value;
if (!nextImageTensor) {
// 新しい画像がない場合は何もしない
requestAnimationFrame(loop);
return;
}
// 1. ポーズを推定
const estimatedPoses = await model.estimatePoses(nextImageTensor);
setPoses(estimatedPoses);
// 2. メモリを解放するためにtensorを dispose
tf.dispose(nextImageTensor);
// 3. 次のフレームにループ
requestAnimationFrame(loop);
};
loop();
};
// ... return文内で、<Camera>を<TensorCamera>に置き換え
<TensorCamera
ref={cameraRef}
style={styles.camera}
type={CameraType.front}
cameraTextureHeight={1920}
cameraTextureWidth={1080}
resizeHeight={200}
resizeWidth={152}
resizeDepth={3}
onReady={handleCameraStream}
autorender={true}
/>
動作の仕組み
cameraWithTensors:これは@tensorflow/tfjs-react-nativeからの高次コンポーネントです。標準のCameraコンポーネントをラップし、onReadyという新しいpropsを提供し、カメラフィードから直接画像tensorのストリームを与えます。これは画像を手動で撮って変換するよりもはるかに効率的です。handleCameraStream:この関数は画像ストリームを受け取ります。ストリームからnextImageTensorを取得する非同期loopを作成します。model.estimatePoses():これがコアのTensorFlow.js関数です。画像tensorを渡すと、検出されたポーズの配列を返します。単一ポーズモデルを使用しているため、この配列には最大1つのポーズオブジェクトが含まれます。tf.dispose():**パフォーマンスに不可欠!**TensorはGPUメモリを消費します。アプリのクラッシュにつながるメモリリークを防ぐために、使用完了時に明示的に disposeする必要があります。requestAnimationFrame(loop):UIスレッドをブロックせずにデバイスが処理できる最快的フレームを処理する効率的なループを作成します。
ステップ4:骨格の描画(ビジュアルフィードバック)
ポーズデータができたので、可視化しましょう!カメラの上にSkia Canvasをオーバーレイし、キーポイントとそれらを結ぶ線を描画します。
何をするか
ポーズデータをpropsとして受け取り、React Native Skiaを使用してキーポイントの円と骨の線を描画する新しいコンポーネントPoseSkeletonを作成します。
実装
まず、src/PoseSkeleton.jsを作成します:
// src/PoseSkeleton.js
import React from 'react';
import { Canvas, Path, Skia, Circle } from '@shopify/react-native-skia';
import { StyleSheet } from 'react-native';
const PoseSkeleton = ({ poses, cameraWidth, cameraHeight }) => {
if (!poses || poses.length === 0) {
return null;
}
// 骨格を形成するためのキーポイント間の接続を定義
const skeletonConnections = [
['nose', 'left_eye'], ['left_eye', 'left_ear'],
['nose', 'right_eye'], ['right_eye', 'right_ear'],
['nose', 'left_shoulder'], ['nose', 'right_shoulder'],
['left_shoulder', 'right_shoulder'],
['left_shoulder', 'left_elbow'], ['left_elbow', 'left_wrist'],
['right_shoulder', 'right_elbow'], ['right_elbow', 'right_wrist'],
['left_shoulder', 'left_hip'], ['right_shoulder', 'right_hip'],
['left_hip', 'right_hip'],
['left_hip', 'left_knee'], ['left_knee', 'left_ankle'],
['right_hip', 'right_knee'], ['right_knee', 'right_ankle'],
];
const pose = poses; // ポーズは1つのみ
const keypoints = pose.keypoints.reduce((acc, keypoint) => {
acc[keypoint.name] = keypoint;
return acc;
}, {});
const path = Skia.Path.Make();
skeletonConnections.forEach(([startName, endName]) => {
const start = keypoints[startName];
const end = keypoints[endName];
if (start && end && start.score > 0.5 && end.score > 0.5) {
path.moveTo(start.x, start.y);
path.lineTo(end.x, end.y);
}
});
return (
<Canvas style={StyleSheet.absoluteFill}>
{Object.values(keypoints).map((p) => {
if (p.score > 0.5) { // 確信度の高いキーポイントのみ描画
return <Circle key={p.name} cx={p.x} cy={p.y} r={5} color="cyan" />;
}
return null;
})}
<Path
path={path}
color="aqua"
style="stroke"
strokeWidth={3}
/>
</Canvas>
);
};
export default PoseSkeleton;
次に、App.jsに統合します:
// App.js (追加部分)
import PoseSkeleton from './src/PoseSkeleton';
// ... Appコンポーネントのreturn文内で、TensorCameraの後に
{poses && poses.length > 0 && (
<PoseSkeleton poses={poses} cameraWidth={width} cameraHeight={height} />
)}
動作の仕組み
PoseSkeletonコンポーネントはposes配列を受け取ります。- 描画する前に、キーポイントの
score(信頼度レベル)が閾値(0.5)を超えているか確認します。 - キーポイントの描画:キーポイントをマッピングして、それぞれに対してSkiaの
<Circle>をレンダリングします。 - 骨格の描画:どの関節を接続するかを定義する
skeletonConnectionsマップを作成します。次にSkiaの<Path>を作成し、moveToとlineToを使用して線を描画し、効果的に点を接続します。 <Canvas>にStyleSheet.absoluteFillを使用して、カメラの尺寸と完全に一致する透明なオーバーレイにします。
今すぐアプリを実行すると、リアルタイムで体にシアンの骨格オーバーレイが表示されるはずです!✨
ステップ5:スクワットフォーム修正ロジックの実装
これがAIトレーナーにおける「トレーナー」部分です。ひざと腰の角度を計算して、スクワットが十分に深いかどうかを判断し、フィードバックを提供します。
何をするか
3点間の角度を計算するヘルパー関数を書きます。次に、メインコンポーネントでこの関数を使用して、ひざ、足首のキepointで腰の角度を測定し、スクワットの深度フィードバックを表示します。
実装
まず、utilsファイルsrc/geometry.jsを作成します:
// src/geometry.js
// 3点間の角度を計算する関数
export function calculateAngle(a, b, c) {
const radians = Math.atan2(c.y - b.y, c.x - b.x) - Math.atan2(a.y - b.y, a.x - b.x);
let angle = Math.abs(radians * 180.0 / Math.PI);
if (angle > 180.0) {
angle = 360 - angle;
}
return angle;
}
次に、App.jsでこれを使用してフィードバックを提供します。
// App.js (追加部分)
import { calculateAngle } from './src/geometry';
// ... Appコンポーネント内
const [feedback, setFeedback] = useState('');
// ポーズが変更されるたびに実行されるこのuseEffect
useEffect(() => {
if (poses && poses.length > 0) {
const keypoints = poses.keypoints.reduce((acc, keypoint) => {
acc[keypoint.name] = keypoint;
return acc;
}, {});
const leftHip = keypoints['left_hip'];
const leftKnee = keypoints['left_knee'];
const leftAnkle = keypoints['left_ankle'];
if (leftHip && leftKnee && leftAnkle &&
leftHip.score > 0.5 && leftKnee.score > 0.5 && leftAnkle.score > 0.5) {
// 左ひざの角度を計算
const kneeAngle = calculateAngle(leftHip, leftKnee, leftAnkle);
// スクワット深度のシンプルなロジック
if (kneeAngle > 160) {
setFeedback('Start Squat');
} else if (kneeAngle < 100) {
setFeedback('Good Squat! 👍');
} else {
setFeedback('Go Lower!');
}
}
}
}, [poses]);
// ... return文内で、フィードバック用のTextコンポーネントを追加
<Text style={styles.feedbackText}>{feedback}</Text>
// ... StyleSheet内に、フィードバックテキストのスタイルを追加
feedbackText: {
position: 'absolute',
bottom: 100,
left: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
fontSize: 24,
padding: 10,
borderRadius: 5,
zIndex: 10,
}
動作の仕組み
calculateAngle:この関数はベクトル演算(atan2)を使用して、3つのキーポイントによって形成される角度を見つけます。これを使用して関節の曲げを測定します。useEffectフック:posesに依存するuseEffect内に分析ロジックを実行します。これにより、新しいポーズが検出されるたびにロジックが実行されます。- フォームロジック:腰、ひざ、足首のキーポイントを取得します。
kneeAngleを計算してシンプルなルールを適用します:- 角度が160°以上の場合は、ユーザーが立っている可能性があります。
- 角度が100°未満の場合は、適切なスクワット深度に達しています。
- それ以外の場合は、もっと低くする必要があります。
- フィードバックは状態に保存され、画面にオーバーレイされた
<Text>コンポーネントに表示されます。
パフォーマンスの考慮事項
- モデルの選択:私たちは
sINGLEPOSE_LIGHTNINGを使用しましたが、それはそれが高速だからです。より高い精度にはsINGLEPOSE_THUNDERを使用できますが、パフォーマンスの低下を覚悟してください。 - Tensor管理:
tf.dispose()の呼び出しを忘れると、メモリリークとアプリクラッシュにつながります。cameraWithTensorsはこれの管理に役立ちますが、手動でtensorを作成する場合は注意が必要です。 - フレームレート:
cameraWithTensorsアプローチは優れていますが、完璧ではありません。一貫した高FPSを必要とする本番グレードのアプリには、JSIベースのフレームプロセッサを使用したreact-native-vision-cameraを検討してください。これにより、カメラスレッドで同期的にJavaScriptコードを実行でき、最大のパフォーマンスを実現します。 - モデルの量子化:さらにパフォーマンスを向上させるには、トレーニング後の量子化を使用してモデルのサイズを縮小し、読み込み時間と推論を高速化できます。
セキュリティのベストプラクティス
このアーキテクチャの最大の利点の1つは、そのプライバシーです。
- デバイス上の処理:すべての画像処理はユーザーのデバイスで行われます。動画や個人データはクラウドに送信されません。これはヘルス&フィットネスアプリにとって重要な機能であり、大きなアピールポイントです。
- 許可:カメラの使用許可を正しくリクエストし、ユーザーが拒否した場合のケースも処理します。許可をリクエストする前に、なぜ必要かを常に説明してください。
結論
おめでとうございます!React Nativeで洗練されたAI搭載フィットネスアプリケーションを構築しました。リアルタイムカメラフィードを取得し、強力なオンデバイスマシンラニングモデルを使用して人間のポーズを理解し、ユーザーがスクワットフォームを改善するための即座で実行可能なフィードバックを提供しました。
これは始まりにすぎません。この基盤に基づいて展開できます:
- 他の運動のロジックを追加(腕立て伏せ、ランジ)。
- _rep counterの実装。
- 完全なワークアウトルーティンを作成。
- 進捗を時間とともに追跡してローカルに保存。
モバイル開発とオンデバイスAIの組み合わせにより知的でプライベートであり、実際に役立つアプリケーションを作成する可能性の世界が広がります。
リソース
クイックリファレンス
| 側面 | 重要な情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と意識向上 |
| アクション | 医療提供者と一緒に議論してください |
| 頻度 | ケアチーム推奨通り |
| エビデンス | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に該当するどうかはどうすればわかりますか? A: 個人的な病歴と家族歴について医療提供者と議論し、個人的なリスク因子を評価し、適切な次のステップを決定してください。
Q: 医師に何を聞くべきですか? A: 良い質問には次のようなものがあります:「私の結果はどういう意味ですか?」「私のオプションは何ですか?」「どのようなライフスタイルの変更が役立ちますか?」「いつフォローアップすべきですか?」
Q: 生活に役立つ変化はありますか? A: ほとんどの健康状態は、基本的な対策から恩恵を受けます:バランスの取れた栄養、定期的な運動(週150分以上)、質のの良い睡眠(7〜9時間)、ストレス管理、タバコの回避。
医療免責事項
この記事は教育目的のみを目的としており、医療アドバイスではありません。健康、治療、ケアに関する決定する前に、常に資格のある医療提供者と相談してください。個々の状況は異なり、一人に適切なことがもう一人に適切なわけではないかもしれません。
救急医療の状況が発生した場合は、すぐに119番(または現地の緊急電話番号)に電話してください。