🏃♂️ はじめに
物置でずっと眠っていた Jetson Nano を、趣味のランニングに活かせたら面白いんじゃないか——。
そんな軽い思いつきから リアルタイムでフォーム解析ができるエッジAIシステム を作成しました。
今回は、Jetson Nano に 市販のアクションカメラを組み合わせて
「走る姿勢をリアルタイム推定して配信する RunningAI」を作ったので、構成と手順をまとめます。
🔧 Jetson Nano の環境構築
Jetson はバージョン依存が強いので、安定した構成をまず書いておきます。
| 項目 | バージョン |
|---|---|
| JetPack | 4.6.6 |
| CUDA | 10.2 |
| PyTorch | 1.10.0(aarch64, CUDA10.2) |
| TensorRT | 8.2 |
| trt_pose | master |
| OpenCV | 4.5.1 |
| FastAPI | 最新 |
| Flask | 3.x |
| Docker Compose | v2.40 |
🏃trt_pose のセットアップ
姿勢推定は NVIDIA の trt_pose を使います。
軽量モデルでも Jetson Nano で十分リアルタイムに動きます。
使用モデル
-
human_pose.json(18関節定義)
-
resnet18_baseline_att_224x224_A_epoch_249.pth(学習済みモデル)
🎥 Flask によるリアルタイムストリーミング
映像の配信はFlask で簡易 Web サーバを立てて、その中で MJPEG ストリームを配信します。詳細な役割は以下の通りです。
- Jetson のカメラ映像を取得
- trt_pose で推定した骨格を描画
- MJPEG として配信
前提条件
実行前に、以下が整っていることを確認します。
- JetPack / L4T: R32.x 系(例: R32.7.6)
- Docker が有効
docker --version
USB カメラが /dev/video0 として認識されている
ls -l /dev/video*
1.ホスト側:プロジェクトディレクトリ作成
まずは Jetson 側(ホスト OS)で作業ディレクトリを切ります。
mkdir -p ~/jetson_projects
ここにコンテナ内 /workspace をマウントして使っていきます。
2.コンテナ起動(GPU / カメラ / ネットワーク対応)
NVIDIA 公式の L4T PyTorch イメージを使って、trt_pose 用の作業コンテナを立ち上げます。
sudo docker run -it --runtime nvidia --network host \
--device /dev/video0:/dev/video0 \
--device /dev/bus/usb:/dev/bus/usb \
--name trt_pose_env \
--volume ~/jetson_projects:/workspace \
nvcr.io/nvidia/l4t-pytorch:r32.7.1-pth1.10-py3 bash
一度作ってしまえば、次回以降はコンテナに再入室するだけでOKです。
sudo docker exec -it trt_pose_env bash
3.コンテナ内の最低限セットアップ
コンテナに入ったら、まず最低限のツールだけ入れておきます。
apt-get update && apt-get install -y nano
4. trt_pose を “develop” で導入(コンテナ内)
コンテナ内で作業します。
develop にしておくと、コードを編集しても即反映されるので開発が楽です。
cd /workspace
git clone https://github.com/NVIDIA-AI-IOT/trt_pose.git
cd trt_pose
python3 setup.py develop
5.タスク用ディレクトリ作成とモデル配置
次に、姿勢推定タスク用のディレクトリを切ります。
mkdir -p /workspace/trt_pose/tasks/human_pose
cd /workspace/trt_pose/tasks/human_pose
学習済みモデルとキーポイント定義を配置します。
wget -O resnet18_baseline_att_224x224_A_epoch_249.pth \
https://github.com/NVIDIA-AI-IOT/trt_pose/releases/download/v0.0.1/resnet18_baseline_att_224x224_A_epoch_249.pth
cat > human_pose.json <<'JSON'
{"supercategory": "person", "id": 1, "name": "person", "keypoints": ["nose", "left_eye", "right_eye", "left_ear", "right_ear", "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", "left_wrist", "right_wrist", "left_hip", "right_hip", "left_knee", "right_knee", "left_ankle", "right_ankle", "neck"], "skeleton": [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13], [6, 8], [7, 9], [8, 10], [9, 11], [2, 3], [1, 2], [1, 3], [2, 4], [3, 5], [4, 6], [5, 7], [18, 1], [18, 6], [18, 7], [18, 12], [18, 13]]}
JSON
このコマンドを実行することで、
姿勢推定で使用する 関節の一覧(keypoints) と、
その関節をどう線で結ぶかという 骨格構造(skeleton) を trt_pose に定義できます。
6. Flask でリアルタイム配信(MJPEG)
ここまでできたら、いよいよリアルタイム配信です。
pythonでFlask配信するためのコード(pose_live_web.py)を書き、それを実行します。
import cv2
import json
import torch
import numpy as np
from flask import Flask, Response
from trt_pose.parse_objects import ParseObjects
from trt_pose.draw_objects import DrawObjects
from trt_pose import models
import torchvision.transforms.functional as F
app = Flask(__name__)
# ----------------------------
# モデル & human_pose 設定
# ----------------------------
with open('human_pose.json', 'r') as f:
human_pose = json.load(f)
num_parts = len(human_pose['keypoints'])
num_links = len(human_pose['skeleton'])
# ResNet18 ベースの姿勢推定モデル
model = models.resnet18_baseline_att(
num_parts,
num_links
)
model.load_state_dict(torch.load(
'resnet18_baseline_att_224x224_A_epoch_249.pth',
map_location='cuda'
))
model = model.cuda().eval()
parse_objects = ParseObjects(human_pose)
draw_objects = DrawObjects(human_pose)
# ----------------------------
# 前処理関数
# ----------------------------
def preprocess(img_bgr):
# trt_pose は 224x224 / RGB 想定
img = cv2.resize(img_bgr, (224, 224))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
tensor = F.to_tensor(img).cuda()
tensor = tensor.unsqueeze(0) # (1,3,224,224)
return tensor
# ----------------------------
# カメラ設定
# ----------------------------
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise RuntimeError("カメラが開けませんでした (/dev/video0 を確認してください)")
# ----------------------------
# MJPEG ストリーム生成
# ----------------------------
def gen_frames():
while True:
ret, frame = cap.read()
if not ret:
continue
# ---- 姿勢推定 ----
with torch.no_grad():
tensor = preprocess(frame)
cmap, paf = model(tensor)
cmap, paf = cmap.detach().cpu(), paf.detach().cpu()
counts, objects, peaks = parse_objects(cmap, paf)
# ---- 骨格描画 ----
draw_objects(frame, counts, objects, peaks)
# ---- JPEG にエンコードして送信 ----
ret, jpeg = cv2.imencode('.jpg', frame)
if not ret:
continue
frame_bytes = jpeg.tobytes()
yield (
b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' +
frame_bytes +
b'\r\n'
)
# ----------------------------
# Flask ルーティング
# ----------------------------
@app.route('/')
def index():
# 超シンプルなページ(img タグで /video を表示)
return """
<html>
<head><title>RunningAI - Pose Stream</title></head>
<body>
<h2>RunningAI Pose Stream (Flask only)</h2>
<img src="/video" />
</body>
</html>
"""
@app.route('/video')
def video():
return Response(
gen_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame'
)
# ----------------------------
# メイン
# ----------------------------
if __name__ == "__main__":
# 0.0.0.0 で公開 → 同一LAN内のPCやスマホからアクセス可能
app.run(host="0.0.0.0", port=5000, threaded=True)
動かし方(コンテナ内)
human_pose.json
resnet18_baseline_att_224x224_A_epoch_249.pth
が、pose_live_web.py と同じディレクトリに置いてあることを確認して下記コードを実行します。
python3 pose_live_web.py
ログに
* Running on http://0.0.0.0:5000/
と出たら、同じネットワークにいる PC / スマホのブラウザから:
http://<JetsonのIP>:5000
にアクセスすれば、そのままFlask だけで骨格つき映像配信が見られます。
無事、左上に骨格だけ抽出できました!
今後はNumPy でベクトル演算して股関節の角度などを数値化して分析を進めて行きたいと思います。
