家に Raspberry Pi が落ちていたので腰痛を改善しようとした

最近、家の片付けをしていたら、昔買ったまま放置していた **Raspberry Pi** が引き出しの奥から出てきました。  
「あれ？これ、何に使おうとしてたっけ…？」としばらく考えたものの、思い出せず。  
せっかくなので、何か役立つことに使ってみよう！と思い立ちました。

で、ちょうど思いついたのが**腰痛予防**。


## きっかけ：デスクワークで腰が悲鳴を上げていた

リモートワークが増えてから、どうにも腰が痛い…。  
特に、気づいたら3時間以上座りっぱなしでパソコンに向かっていることが多いのです。

ふとした瞬間に立ち上がろうとすると、「イタタ…」と腰に痛みが走ることもしばしば。  
原因を考えてみると、どうやら次のような問題がありそうです：

- 長時間座りっぱなしで姿勢が悪くなる
- 運動不足で筋力が低下している
- ストレッチをしようと思っても、つい忘れてしまう
  
もちろん、市販のガジェットやアプリを使って改善を目指す手もありますが、どうせなら手元にある Raspberry Pi を活用して、オリジナルの「腰痛予防システム」を作ってみよう！と思い立ったのです。


## 体に悪い姿勢とは

デスクワーク中の悪い姿勢は、長時間の作業によって筋肉や骨に無理な負荷をかけ、体にさまざまな影響を及ぼします。以下に代表的な例を挙げてみます：

### よくある悪い姿勢
1. 猫背  
    背中が丸まり、肩が前に出ている状態。背骨に負担がかかり、肩こりや腰痛の原因になります
2. 前傾姿勢  
    頭を前に突き出したような姿勢。頭の重さが首や肩、腰に集中し、筋肉が緊張します
3. 足を組む癖  
    骨盤が歪み、左右の筋肉バランスが崩れる原因に

### 姿勢が悪いと何が起こる？  
悪い姿勢を続けると、腰痛だけでなく以下のような症状も引き起こされることがあります：

- 肩こりや首の痛み
- 血行不良によるむくみや疲労感
- 集中力の低下や倦怠感

これらを防ぐためには、「正しい姿勢を意識し続けること」と「定期的に体を動かすこと」が重要です。
ただし、言うは簡単、実行するのは難しいですよね…。

ここで Raspberry Pi の出番です！

## Raspberry Pi とは
Raspberry Pi（ラズベリーパイ）は、小さなコンピュータで、手のひらに収まるコンパクトなサイズながらも、通常のパソコンのような機能を持っています。  
特に注目すべき特徴を以下にまとめました：

- 低価格で手に入る（数千円程度）
- プログラムの自由度が高く、自作のシステムやガジェットを作ることができる
- 省電力設計で、長時間稼働が可能
  
もともとは教育目的で作られたものですが、現在ではDIYガジェットの基盤としても非常に人気です。
例えば、以下のような用途で使われています：

- IoT機器：スマートホームデバイスの制御
- メディアプレイヤー：音楽や動画を再生する専用端末
- ロボット制御：センサーを組み合わせて動きをプログラム
  
私の場合は、「腰痛予防のための姿勢判定システム」という活用法を目指してみました。

## 準備

### 用意したもの
- Raspberry Pi 4 Model B
- micro SD カード 32GB
  - Raspberry Pi のストレージ
- Anker 2-in-1 USB 3.0 ポータブルカードリーダー
  - micro SD カード内のファイルを別PCで編集するため
- Logicool C920n HD Pro ウェブカメラ
  - USB接続カメラ

### Raspberry Pi 設定
1. OS (Raspbian) の準備  
   - 購入してから時間がたった Raspberry Pi だったため OS が古かった (python 3.7)
   - [Raspberry Pi Imager](https://www.raspberrypi.com/software/) で最新の OS を micro SDカードに書き込む
2. モニターに出力する
   - 起動しても出力されなかった
   - `config.txt` のHDMI接続関連を編集したら映るようになった
   - Wi-Fiをつないでおく
   - 参考： [【初心者向け】Raspberry pi がHDMIで映らない?解決策](https://qiita.com/paupau/items/44c86b02f6d475d1a288)
3. SSH で Raspberry Pi に接続する
   - micro SDカードに `ssh` という名の空ファイル作ると自動的にSSH機能がONになる
   - Raspberry Pi のIPアドレスを私用PCで特定する
     - `for /l %i in (0.1.255) do ping -w 1 -n 1 192.168.0.%i` をコマンドプロンプトで実行
     - `arp -a` でネットワークに使われているIPアドレスの一覧が表示
     - MACアドレスの先頭が `b8-27-eb` もしくは `dc-a6-32` の列が Raspberry Pi に関する情報
   - `ssh pi@{IPアドレス}` でssh接続
4. VNC機能の設定
   - 私用PCで Raspberry Pi の画面をGUIベースで遠隔操作できるようになる
   - ssh接続後、`sudo raspi-config` -> `Interfacing Options` -> `VNC` -> `YES` で有効化
   - 私用PCに [VNC Viewer](https://www.realvnc.com/en/connect/download/viewer/) をインストール
   - VNC Viewer に Raspberry Pi のIPアドレスを打ち込むと遠隔操作できるようになる！
5. USBでカメラが接続できるかを確認
   - `ls /dev/video*` でつながっているか確認できる
   - `cap = cv2.VideoCapture(0)` でカメラが動作するか確認できる
 
![vnc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840383/54d4cc44-580d-e2c0-d2d2-7133f529494d.png)

## いざ実装 
### 姿勢検知モデル
今回は Raspberry Pi で動かすため、比較的軽量な [MoveNet](https://www.kaggle.com/models/google/movenet/tfLite) を使いました。  
MoveNetは、人間の体の動きをリアルタイムで検出するために設計されたAIモデルです。カメラに映った人の体の各関節を特定し、その位置を即座に把握することで、動作の解析や追跡を可能にします。  
頭、肩、ひじ、手首、腰、ひざ、足首など、人体の17か所を正確に検出します。この関節情報から悪い姿勢を検出します！

### 悪い姿勢を検出する
時間がなかったので凄くシンプルに実装します。  
今回は **猫背** と **前方頭部姿勢** の検知を行います。
カメラは横からの画角に固定している前提でルールを作っていきます。

### 猫背
使うポイントは 左肩と右肩の中心・左腰と右腰の中心です。
肩から腰にかけての垂直角度が80度未満の場合に検出します

``` python
def calculate_vertical_angle(a, b):
    
    a = np.array(a)
    b = np.array(b)

    vector = a - b

    vertical_vector = np.array([0, -1])
    cosine_angle = np.dot(vector, vertical_vector) / (np.linalg.norm(vector) * np.linalg.norm(vertical_vector))
    angle = np.degrees(np.arccos(np.clip(cosine_angle, -1.0, 1.0)))

    return angle

shoulders_mid = (left_shoulder + right_shoulder) / 2
hips_mid = (left_hip + right_hip) / 2

shoulder_to_hip_angle = calculate_vertical_angle(shoulders_mid, hips_mid)
```

### 前方頭部姿勢
使うポイントは 鼻・左肩と右肩の中心・左腰と右腰の中心です。  
鼻と肩と腰を結んだときにできる角度が125度未満場合に検出します。

``` python
def calculate_angle(a, b, c):
    a = np.array(a)
    b = np.array(b)
    c = np.array(c)

    ba = a - b
    bc = c - b

    cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
    angle = np.arccos(np.clip(cosine_angle, -1.0, 1.0))
    return np.degrees(angle)

shoulders_mid = (left_shoulder + right_shoulder) / 2
hips_mid = (left_hip + right_hip) / 2

neck_angle = calculate_angle(nose, shoulders_mid, hips_mid)
```

### あとはモデルの設定なりカメラの設定をササっと書く
``` python
import tensorflow as tf
import tflite_runtime.interpreter as tflite
import numpy as np
import cv2
import os
from matplotlib import pyplot as plt

model_path = "path_to_model"
input_size = 192

interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

def movenet(input_image):
    input_image = tf.cast(input_image, dtype=tf.uint8)
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    interpreter.set_tensor(input_details[0]['index'], input_image.numpy())
    interpreter.invoke()
    keypoints_with_scores = interpreter.get_tensor(output_details[0]['index'])
    return keypoints_with_scores


def draw_prediction_on_image(image, keypoints_with_scores, threshold=0.25):
    keypoints = keypoints_with_scores[0][0][:, :3]
    height, width, _ = image.shape


    face_indices = [0, 1, 2, 3, 4]  # [nose, left eye, right eye, left ear, right ear]
    body_indices = [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]  # shoulders, elbows, wrists, etc.

    for i, kp in enumerate(keypoints):
        y, x, confidence = kp
        if confidence > threshold:
            cx, cy = int(x * width), int(y * height)

            if i in face_indices:
                color = (0, 0, 255)
            elif i in body_indices:
                color = (0, 255, 0)
            else:
                color = (255, 255, 0)

            cv2.circle(image, (cx, cy), 5, color, -1)
    return image


def calculate_angle(a, b, c):
    a = np.array(a)
    b = np.array(b)
    c = np.array(c)

    ba = a - b
    bc = c - b

    cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
    angle = np.arccos(np.clip(cosine_angle, -1.0, 1.0))
    return np.degrees(angle)


def calculate_vertical_angle(a, b):
    
    a = np.array(a)
    b = np.array(b)

    vector = a - b

    vertical_vector = np.array([0, -1])
    cosine_angle = np.dot(vector, vertical_vector) / (np.linalg.norm(vector) * np.linalg.norm(vertical_vector))
    angle = np.degrees(np.arccos(np.clip(cosine_angle, -1.0, 1.0)))

    return angle


def evaluate_posture(keypoints_with_scores):
    keypoints = keypoints_with_scores[0][0][:, :3]

    nose = keypoints[0][:2]
    left_shoulder = keypoints[5][:2]
    right_shoulder = keypoints[6][:2]
    left_hip = keypoints[11][:2]
    right_hip = keypoints[12][:2]

    shoulders_mid = (left_shoulder + right_shoulder) / 2
    hips_mid = (left_hip + right_hip) / 2

    shoulder_to_hip_angle = calculate_vertical_angle(shoulders_mid, hips_mid)
    neck_angle = calculate_angle(nose, shoulders_mid, hips_mid)

    posture_issues = []
    if neck_angle < 125:
        posture_issues.append("Forward head posture detected. Angle: {neck_angle:.2f} degrees")
    if shoulder_to_hip_angle < 80 or shoulder_to_hip_angle > 105:
        posture_issues.append(f"Rounded shoulders detected. Angle: {shoulder_to_hip_angle:.2f} degrees")
    
    return posture_issues


cap = cv2.VideoCapture(0)
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    input_image = tf.image.resize_with_pad(tf.convert_to_tensor(frame), input_size, input_size)
    input_image = tf.expand_dims(input_image, axis=0)

    keypoints_with_scores = movenet(input_image)
    posture_issues = evaluate_posture(keypoints_with_scores)
    output_overlay = draw_prediction_on_image(frame, keypoints_with_scores)

    y_offset = 50 
    for issue in posture_issues:
        cv2.putText(output_overlay, issue, (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
        y_offset += 30

    cv2.imshow('MoveNet Pose Detection with Posture Evaluation', output_overlay)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
```

### 結果
リアルタイムで検出することができるようになりました！
#### 良い姿勢
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840383/e9602c46-6883-c7e4-31bb-1e84a022e6f4.png">

#### 悪い姿勢（猫背）
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840383/d6838b2f-64ac-a067-6be5-e5a17692f8f6.png">

#### 悪い姿勢（前方頭部姿勢）
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840383/47996769-6d14-cad4-5eb7-f4e14a6fab80.png">

### もっと改善できそう
今回はシンプルなルールベースで実装したため、悪い姿勢が検出できなかったり、良い姿勢であるにもかかわらず悪い姿勢で検出されてしまうことがあります。

- 時間軸データを使う
- 他の点も利用し、姿勢が悪い特徴をもっと得る
- 機械学習モデルを使う

これらを利用すればもっと良そさうな姿勢検知システムが作れそうですね！

## まとめ：健康こそがエンジニアの基盤
### 腰痛予防はできそうだけど治療はできない
腰痛になったら Raspberry Pi を触る前に、一度整体もしくは整形外科に行った方がよさそうです。実装していたり、Qiitaを書いてる間も腰を痛めてしまいました。

### 健康である限り、知識も技術も無限に磨ける
これが言いたくてこの記事を書いたまであります。  
どれだけ素晴らしいアイデアや技術があっても、それを実現する体と心がなければ本末転倒です。エンジニアにとって、健康であることはキャリアの持続性を支える基盤であり、学び続け、進化し続けるための最大の武器です。  健康を維持することで、知識や技術の吸収力も、仕事への集中力も、問題解決能力も、すべてが飛躍的に向上します。  
デスクワークは、エンジニアにとって避けられないライフスタイルの一部ですが、それをただ座りっぱなしの負担としてではなく、より快適で健康的な「デスクワークライフ」に変えていきましょう。良い姿勢や適度な運動、ストレッチを取り入れるだけで、日々の生産性も心身の充実感も大きく変わります。