ishigenの技術ブログ

卓球×テクノロジー、機械学習

フォームの可視化と再現性の計算①

今回は前回の記事の動画を用いて、フォームの可視化と再現性の計算を行います。とりあえず動くコードでしかないので、後々もう少しきれいにします。

ishigentech.hatenadiary.jp

環境

ubuntu 16.04.04
python 3.6.6
jupyter notebook

フォームの可視化について

全身が写っているものから始めるのがやりやすかったため、今回はカットです。そのうちドライブとかチキータとでもやりたいとは思います。

openposeについて

前回の記事でも書きましたが、openposeは骨格を推定するものです。下記画像のように体の各ポイント18点を推定します。
f:id:ishigentech:20180916140807p:plain
公式より

可視化の結果

出力された数値を用いて、体の動きをグラフにするとこうなります。各ポイント体の動きを折れ線で表現しています。
1を原点にしているので、全て0にしています。f:id:ishigentech:20180916150312p:plain3が右肘なのでカットの動きになっています。f:id:ishigentech:20180916150316p:plain4は右手首で3と同様です。f:id:ishigentech:20180916150319p:plainf:id:ishigentech:20180916150323p:plainf:id:ishigentech:20180916150327p:plainf:id:ishigentech:20180916150331p:plain12は左膝です。右膝と比較しても意外と動いています。高さを調節しているのかもしれません。f:id:ishigentech:20180916150334p:plainf:id:ishigentech:20180916150337p:plain16は欠損値が多いかつ右耳だったので、全て同じ値にしました。f:id:ishigentech:20180916150340p:plain

体のポイントごとの動きなので、変な癖とかがあるとすぐにわかりそうではあります。フォームの研究としてはふさわしくないと思います。

データの前処理

前処理の手順

  1. 欠損値、外れ値の処理
  2. どこからどこまでがカットなのかを判断
  3. 欠損値、外れ値の処理
  4. 横ずれの調整
import json
import numpy as np
import matplotlib.pyplot as plt
import os
from pylab import *

データの読み込み
json形式で保存されているファイルを読み込みます。

data = np.array([])
path = 'パス'
files = os.listdir(path)
files = sort(files)
for file in files:
    filename = path +'/'+ file
    f = open(filename)
    val = json.load(f)
    f.close()
    val = np.array(val['people'][0]['pose_keypoints_2d'])
    data = np.append(data, val)
data = data.reshape(len(files), 18, 3)


1. 外れ値がそれなりにあるので、分割する前に中央値で補完します。二回に分かれてしまっているのは、対応方法が異なるからです。

for i, ivalue in enumerate(data):
    if i <= 1: continue
    for k,kvalue in enumerate(ivalue):
        for t, tvalue in enumerate(kvalue):
            if t >= 1:
                medivalue = data[i-2:i+3, k, t]
                data[i, k, t] = np.median(medivalue)
    if i >= 580: break


2. まず厳密な区分は存在しません。ただこの計算には一律の評価基準があればいいので、今回は目で見て判断しました。自分で判断したのでフレーム単位で見るとノイズが含まれているのは間違いありません。20フレームを1スイングとして抽出しました。7回打球しているので(7*20*18*3)の四次元のデータを使います。
※(スイング数*1スイングあたりのフレーム数*推定される体のポイントの数*x座標、y座標、スコア)

swingperframe = 20
swing = np.array([])
index = np.array([140,208,277,343,412,485,558])
for i in index:
    swing = np.append(swing, data[i:i+swingperframe,:,:])
swing = swing.reshape(len(index),swingperframe, 18, 3)
print(swing.shape)

3. 3フレーム以上連続で欠損値を取るデータがあり、これらは上記の中央値での補完処理では対応しきれないので、分割してから平均値で修正します。

for i,ivalue in enumerate(swing):
    for k,kvalue in enumerate(ivalue):
        for t, tvalue in enumerate(kvalue):
            for w, wvalue in enumerate(tvalue):
                if wvalue == 0 and w <= 1:
                    swing[i, k, t, w] = np.mean(swing[:, k, t, w])

4.鎖骨の中心あたりのポイントを原点として調整し、横への移動の影響を除きます。この処理で画面の中で横に移動してスイングすることがあっても、影響を除くことができます。

for i in range(7):
    for k in range(20):
        originx = swing[i, k, 1, 0]
        originy = swing[i, k, 1, 1]
        swing[i, k, :, 0] -= originx
        swing[i, k, :, 1] -= originy

これでデータを確認するとポイント16にいくつか欠損値があり、ポイント16は右耳であり、重要なデータではないので、影響を除きます。

swing[:, :, 16, 0] = swing[:,:,16,0].mean()
swing[:, :, 16, 1] = swing[:, :, 16, 1].mean()

以上で前処理は終了です。

グラフへの描画方法

以下を実行すると、冒頭のグラフが描画されます。

%matplotlib inline

for k in range(9):
    plt.figure(figsize=(15,15))
    for i in range(2):
        plt.subplot(1,2,i+1)
        for t in range(7):
            x = swing[t,:,i+k*2,0]
            y = swing[t,:,i+k*2,1]*-1
            plt.plot(x,y)
            plt.ylim(np.min(swing[:,:,:,1] * -1),np.max(swing[:,:,:,1] * -1))
            plt.xlim(np.min(swing[:,:,:,0]),np.max(swing[:,:,:,0]))
            plt.title(str(i + k * 2), fontsize=20)
            plt.grid(True)
        plt.grid(True)
    #filename = 'plot'+ str(k)
    #plt.savefig(filename)
    plt.show()

再現性の計算

再現性の計算とか難しそうな書き方してますが、結局はどれだけフォームが固まっているかってことです。どうやって求めるかというと、上記のグラフの線がどれだけばらついているかを計算します。

計算方法

  1. 同じフレーム、同じ体のポイントでスイング間のばらつきを比較する
  2. 集計と指標の計算

1. バラつきの計算は同じフレーム、同じ体のポイントの7つの点の分散を求めています。resultに(20*18*2)のデータが入ります。

result = np.array([])
for i in range(20):
    for k in range(18):
        x = swing[:, i, k, 0]
        y = swing[:, i, k, 1]
        meanx = x.mean()
        meany = y.mean()
        result = np.append(result,sum((x - meanx) ** 2))
        result = np.append(result,sum((y - meany) ** 2))
result = result.reshape(20,18,2)

2. 計算の精度はopenposeから出力されるスコアの平均を取れば求められます。ついでに結果ももう少し見やすくします。各点の標準偏差の平均値を集計し、さらにその平均値を全体のずれとして出してみました。現段階ではこの数値が高いのか低いのかは判断がつかない状態であり、意味をなさないものになっています。これから対応したいところです。

print('精度')
print(swing[:,:,:,2].mean())
print('各ポイントのずれ')
aggregate = np.array([])
for i in range(18):
    print(i,sqrt(result[:, i, 0]).mean(),sqrt(result[:, i, 1]).mean())
    aggregate = np.append(aggregate,sqrt(result[:, i, 0]).mean())
    aggregate = np.append(aggregate,sqrt(result[:, i, 1]).mean())
aggregate = aggregate.reshape(18, 2)
print('全体のずれ')
print(aggregate.mean())

出力結果

精度
0.839300687936508
各ポイントのずれ
0 9.50189204164633 10.564456302273527
1 0.0 0.0
2 9.282807860849129 7.985441214121016
3 14.702756879996823 10.611583195300145
4 23.20150323797283 22.482897128714875
5 8.508900989408707 7.900002342359047
6 11.781042222499767 13.336132157249125
7 15.929800657273452 14.729540493196364
8 16.141903539345996 11.318877266007219
9 27.38270955353574 15.6975989054097
10 59.53738866599265 21.545035691419343
11 15.903026407091605 15.729575436582426
12 37.45575582965355 27.12303847364089
13 55.11463401664059 28.103541255376577
14 9.363773603068832 11.410209261304832
15 8.897818944743424 11.028936257261567
16 9.399596873523532e-15 0.0
17 8.009300705809611 8.791248233536667
全体のずれ
15.807586910257843

課題

  • 評価尺度が曖昧

もっとたくさんのデータがないと今回計算した数値がどのくらいのものなのかが全くわかりません。求めた標準偏差ピクセル単位なので、身長との比率とかを取れば、cm単位では出せるかもしれませんが、、、

  • データが少ない

毎回課題になりますが、やはり7スイングだとデータが少なく、100スイングくらいは揃えたほうがいいと思います。ただ、フレーム単位でどこからがスイングの始まりなのかを判断するのに手間がかかります。

  • まだまだノイズが含まれている

横ずれだけは対応しましたが、他にも様々なノイズが含まれていると思います。30fpsだとスイングスピードが微妙に速かったりすると、おそらく誤差になってしまいます。

  • 卓球台が邪魔

冗談ではなく、この分析の際には卓球台が本当に邪魔で、正面から全身が映る映像を取れるプレーが限られてしまいます。この辺も考えていかないといけません。

まとめ

今回は以上です。
再現性の計算は課題がたくさんありますが、可視化するほうは使えたりするんじゃないかと思っています。体の部位ごとに分割してフォームを見たりすることもないと思うので、やってみると面白いかもしれません。