はじめに
前回Kinoveaを使用して算出した歩行中の各ランドマークをグラフにプロットし、関節角度を求めていきたいと思います。
※元動画の私です
左側下肢の各ランドマークの軌跡がグラフにプロットできていると思います。
左側踵接地から2回目の左踵接地までを切り出しています。
前回の内容
- Kinoveaを使用した映像内マーカーのトラッキング
- カメラ撮影による誤差範囲の把握
- 角関節角度の算出に必要なランドマークの検討
- 算出パラメータの検討
- PythonライブラリOpenPoseによる姿勢定位の紹介
今回の内容
- 前額面、左矢状面、右矢状面の各ランドマークのプロット直線の描画による視覚化
- 体幹、股関節、膝関節、足関節の各運動方向の角度算出
- 両肩峰、ASIS、膝蓋骨中央の位置から各傾斜角度を算出
環境構築
歩行時のランドマーク座標のcsvデータを含め、動作解析の過程をGithubで公開しています。
Dockerで環境を作りJupyterLabを使って順番に算出していこうかと思います。
FROM python:3.9.7-buster
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
tzdata \
libgl1-mesa-glx \
&& ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV TZ=Asia/Tokyo
RUN python3 -m pip install --upgrade pip \
&& pip install --no-cache-dir \
black \
jupyterlab \
jupyterlab_code_formatter \
jupyterlab-git \
lckr-jupyterlab-variableinspector \
jupyterlab_widgets \
ipywidgets \
import-ipynb
RUN pip install --no-cache-dir \
numpy \
pandas \
scipy \
matplotlib \
japanize_matplotlib \
seaborn \
requests \
Pillow
データ分析
歩行中のイベント時間
KinoveaのKey Image機能を使って切り出した各歩行周期のイベント時間を使用
下記の時間をもとに1歩行周期を切り出します。
key_image = pd.read_csv("/workspace/walkAnalysis/pointData/keyImage.csv")
# NaNを空白文字列に置き換える
key_image = key_image.fillna("")
key_image
Front | Time | Side | Time.1 | |
---|---|---|---|---|
0 | 右踵接地 | 5.1 | 左踵接地 | 3.34 |
1 | 右全足底接地 | 5.2 | 左全足底接地 | 3.47 |
2 | 左足趾離地 | 5.34 | 対側足趾離地 | 3.60 |
3 | 左踵接地 | 5.74 | 対側踵接地 | 4.04 |
4 | 左全足底接地 | 5.87 | 対側全足底接地 | 4.17 |
5 | 右足趾離地 | 5.9 | 左足趾離地 | 4.24 |
6 | 右踵接地(2回目) | 6.34 | 左踵接地(2回目) | 4.67 |
7 | 右全足底接地 | 6.47 | 右踵接地 | 10.34 |
8 | 左足趾離地(2回目) | 6.54 | 右全足底接地 | 10.44 |
9 | 左踵接地(2回目) | 6.97 | 対側足趾離地 | 10.54 |
10 | 対側踵離地 | 10.94 | ||
11 | 対側全足底接地 | 11.08 | ||
12 | 右足趾離地 | 11.11 | ||
13 | 右踵接地 | 11.54 |
ランドマーク座標の視覚化
歩行中の下記ランドマークx,y座標
右矢状面のランドマーク座標
right_side = pd.read_csv("/workspace/walkAnalysis/pointData/sideRightTrim.csv")
right_side = right_side[(right_side['t'] >= 10.34) & (right_side['t'] <= 11.54)]
right_side.head()
t | R_shoulder_x | R_shoulder_y | L_shoulder_x | L_shoulder_y | R_asis_x | R_asis_y | L_asis_x | L_asis_y | R_knee_x | R_knee_y | L_knee_x | L_knee_y | R_foot_x | R_foot_y | L_foot_x | L_foot_y | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 5.14 | -123.98 | 167.38 | 18.45 | 162.79 | -113.57 | -10.49 | -4.45 | -4.84 | -97.83 | -182.18 | -27.57 | -179.59 | -95.89 | -325.78 | -40.76 | -307.55 |
1 | 5.17 | -124.98 | 168.38 | 17.45 | 161.80 | -114.58 | -10.49 | -5.45 | -6.84 | -97.82 | -184.17 | -27.55 | -181.59 | -94.29 | -331.05 | -40.76 | -307.55 |
2 | 5.20 | -124.98 | 170.38 | 15.45 | 162.80 | -116.58 | -10.49 | -7.44 | -7.84 | -98.81 | -184.19 | -26.57 | -188.61 | -94.66 | -334.34 | -39.76 | -307.55 |
3 | 5.24 | -132.98 | 167.38 | 14.13 | 164.80 | -118.56 | -7.49 | -8.44 | -7.84 | -100.82 | -184.20 | -26.58 | -189.61 | -93.44 | -335.33 | -40.76 | -301.22 |
4 | 5.27 | -133.97 | 172.38 | 12.51 | 168.42 | -120.55 | -3.49 | -9.44 | -6.84 | -101.84 | -182.20 | -26.59 | -189.61 | -93.45 | -335.33 | -37.94 | -295.30 |
$\hspace{88em}$ |
左矢状面のランドマーク座標
left_side = pd.read_csv("/workspace/walkAnalysis/pointData/sideLeftTrim.csv")
left_side = left_side[(left_side['t'] >= 3.34) & (left_side['t'] <= 4.67)]
left_side.head()
t | L_ancle_x | L_ancle_y | L_heel_x | L_heel_y | L_toe_x | L_toe_y | L_leg_x | L_leg_y | L_knee_x | L_knee_y | L_hip_x | L_hip_y | L_asis_x | L_asis_y | L_shoulder_x | L_shoulder_y | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 3.37 | 345.83 | -389.40 | 348.67 | -422.05 | 290.12 | -404.02 | 374.13 | -219.73 | 373.50 | -208.10 | 461.05 | -28.42 | 423.92 | 24.78 | 482.62 | 263.05 |
1 | 3.40 | 340.85 | -392.40 | 346.68 | -422.04 | 286.27 | -414.14 | 365.13 | -223.73 | 364.51 | -208.11 | 448.05 | -31.42 | 411.91 | 22.79 | 473.61 | 261.06 |
2 | 3.44 | 335.85 | -393.42 | 344.67 | -422.07 | 283.43 | -420.21 | 355.13 | -224.73 | 356.51 | -208.11 | 433.05 | -32.42 | 399.90 | 21.81 | 463.61 | 259.05 |
3 | 3.47 | 333.84 | -394.43 | 346.74 | -423.04 | 281.51 | -423.25 | 344.52 | -228.60 | 346.51 | -208.12 | 415.05 | -31.42 | 387.91 | 22.83 | 452.61 | 256.05 |
4 | 3.50 | 332.82 | -395.43 | 346.74 | -422.06 | 279.51 | -424.23 | 338.52 | -228.60 | 338.59 | -209.75 | 409.29 | -30.42 | 375.92 | 24.85 | 441.62 | 254.05 |
$\hspace{88em}$ |
前額面のランドマーク座標
front = pd.read_csv("/workspace/walkAnalysis/pointData/frontTrim.csv")
# 歩行周期の時間に合わせデータフレームを切り出す
front = front[(front['t'] >= 5.10) & (front['t'] <= 6.97)]
front.head()
t | R_shoulder_x | R_shoulder_y | L_shoulder_x | L_shoulder_y | R_asis_x | R_asis_y | L_asis_x | L_asis_y | R_knee_x | R_knee_y | L_knee_x | L_knee_y | R_foot_x | R_foot_y | L_foot_x | L_foot_y | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 5.14 | -123.98 | 167.38 | 18.45 | 162.79 | -113.57 | -10.49 | -4.45 | -4.84 | -97.83 | -182.18 | -27.57 | -179.59 | -95.89 | -325.78 | -40.76 | -307.55 |
1 | 5.17 | -124.98 | 168.38 | 17.45 | 161.80 | -114.58 | -10.49 | -5.45 | -6.84 | -97.82 | -184.17 | -27.55 | -181.59 | -94.29 | -331.05 | -40.76 | -307.55 |
2 | 5.20 | -124.98 | 170.38 | 15.45 | 162.80 | -116.58 | -10.49 | -7.44 | -7.84 | -98.81 | -184.19 | -26.57 | -188.61 | -94.66 | -334.34 | -39.76 | -307.55 |
3 | 5.24 | -132.98 | 167.38 | 14.13 | 164.80 | -118.56 | -7.49 | -8.44 | -7.84 | -100.82 | -184.20 | -26.58 | -189.61 | -93.44 | -335.33 | -40.76 | -301.22 |
4 | 5.27 | -133.97 | 172.38 | 12.51 | 168.42 | -120.55 | -3.49 | -9.44 | -6.84 | -101.84 | -182.20 | -26.59 | -189.61 | -93.45 | -335.33 | -37.94 | -295.30 |
$\hspace{88em}$ |
各座標間を結ぶ直線を描画する
各身体部位に対応するカラム名を配列にしてループさせmatplotlibを使い直線を描画しました。
下記は前額面ですが、これを左右矢状面でも実施しました。
# グラフの描画
plt.figure(figsize=(10, 6))
plt.title('前額面')
plt.xlabel('X')
plt.ylabel('Y')
plt.legend()
plt.axis('equal')
plt.grid(True)
landamrks = ['shoulder','asis','knee','foot']
previous_landmark = None
for landmark in landamrks:
plt.scatter(front[f'R_{landmark}_x'], front[f'R_{landmark}_y'], marker='.', s=5)
plt.scatter(front[f'L_{landmark}_x'], front[f'L_{landmark}_y'], marker='.', s=5)
# 各点をつなぐ線を描画(斜度を算出する直線)
for i in range(len(front[f'R_{landmark}_x'])):
if landmark == 'foot':
break
plt.plot([front[f'R_{landmark}_x'][i], front[f'L_{landmark}_x'][i]],
[front[f'R_{landmark}_y'][i], front[f'L_{landmark}_y'][i]],
color='red', linestyle='-', linewidth=0.1)
if previous_landmark == None:
previous_landmark = landmark
else:
# 各点をつなぐ線を描画(身体部位に対応する直線)
for i in range(len(front[f'R_{landmark}_x'])):
plt.plot([front[f'R_{landmark}_x'][i], front[f'R_{previous_landmark}_x'][i]],
[front[f'R_{landmark}_y'][i], front[f'R_{previous_landmark}_y'][i]],
color='blue', linestyle='-', linewidth=0.1)
plt.plot([front[f'L_{landmark}_x'][i], front[f'L_{previous_landmark}_x'][i]],
[front[f'L_{landmark}_y'][i], front[f'L_{previous_landmark}_y'][i]],
color='blue', linestyle='-', linewidth=0.1)
previous_landmark = landmark
各関節角度の算出
下記のように各角度を求める事ができます。
部位 | 運動方向 | 基本軸と移動軸 |
---|---|---|
体幹 | 前傾/後傾 | 肩峰-大転子と平面座標の垂直軸のなす角 |
股関節 | 屈曲/伸展 | 大転子-膝関節と腓骨頭-外踝のなす角 |
外転/内転 | 肩峰-上前腸骨棘と上前腸骨棘-膝蓋骨中央のなす角 | |
膝関節 | 屈曲/伸展 | 大転子-大腿骨外側顆と腓骨頭-外踝のなす角 |
膝内反角度(FTA) | 上前腸骨棘-膝蓋骨中央と膝蓋骨中央-外内踝中央のなす角 | |
足関節 | 底屈/背屈 | 腓骨頭‐大腿骨外側顆と踵骨-第五中足骨のなす角 |
角度算出用の関数作成
2点間を結ぶ直線とX軸とのなす角を算出する関数
# 4点の座標から成る2直線のなす角を求める関数
def calculate_angle_between_lines(x1_1, y1_1, x2_1, y2_1, x1_2, y1_2, x2_2, y2_2):
# 直線1の傾きを計算
m1 = (y2_1 - y1_1) / (x2_1 - x1_1)
# 直線2の傾きを計算
m2 = (y2_2 - y1_2) / (x2_2 - x1_2)
# 2つの直線のなす角を計算
angle_rad = math.atan(abs((m2 - m1) / (1 + m1 * m2)))
# ラジアンから度に変換
angle_deg = math.degrees(angle_rad)
return angle_deg
2点を結ぶ直線同士のなす角を求める関数
# 2点のX,Y座標を与え2点を結ぶ直線とX軸とのなす角を求める関数
def calculate_angle(x1, y1, x2, y2):
# 2点の座標からベクトルを計算
dx = x2 - x1
dy = y2 - y1
# アークタンジェントを使用して角度を計算(ラジアン)
angle_rad = math.atan2(dy, dx)
# ラジアンから度に変換
angle_deg = math.degrees(angle_rad)
return angle_deg
角度算出をデータフレームの各レコードに使用し動画フレーム毎に関節角度、水平角度を算出する
# 左股関節角度を算出する例
left_side['L_hip_angle'] = left_side.apply(lambda row: calculate_angle_between_lines(row['L_shoulder_x'], row['L_shoulder_y'], row['L_hip_x'], row['L_hip_y'],row['L_hip_x'], row['L_hip_y'], row['L_knee_x'], row['L_knee_y']), axis=1)
# 両肩を結んだ直線の水平角度を算出する例
front['shoulder_horizonal_angle'] = front.apply(lambda row: calculate_angle(row['R_shoulder_x'], row['R_shoulder_y'], row['L_shoulder_x'], row['L_shoulder_y']), axis=1)
各データフレームに角度データが追加された
- hip_angle(股関節角度)
- knee_angle(膝関節角度)
- ancle_angle(足関節角度)
- shoulder_horizonal_angle(両肩峰水平角度)
- asis_horizonal_angle(両ASIS水平角度)
- FTA(膝内反角度)
関節角度・水平角度の視覚化
各データフレームは踵接地の時間を0として同期しているため、歩行中のイベント時間のずれや左右差といった情報が視覚化できています。
もっと分かりやすく各イベントの時間帯にマーカーを付けてもいいのですが、それは検証や考察の段階で行う事にします。
矢状面の各関節角度
前額面の各水平角度,FTA
左右を比較した時に明らかに関節角度の変化や左右の水平角度に傾きがありそうです。
ここから角加速度やランドマークの移動速度、加速度などのパラメータを算出できます。
データの平滑化の処理も必要になりそうです。
まとめ
- 前額面、左矢状面、右矢状面の各ランドマークのプロット直線の描画による視覚化
- 体幹、股関節、膝関節、足関節の各運動方向の角度算出
- 両肩峰、ASIS、膝蓋骨中央の位置から各傾斜角度を算出
余談ですが、今回の結果を受けて自分の身体の不均等さにショックを受けています。
引き続き結果を整理していき私の歩行の何がどう悪いのか検証していきたいと思います。