ishigenの技術ブログ

卓球 機械学習 競ぷろ

サーブ・レシーブ得点率から見るTリーグの勢力図(個人編)

今回は前回の記事で取得したデータを用いて、サーブ・レシーブ得点率を可視化していきます。

ishigentech.hatenadiary.jp

サーブ時の得点率についてはランキング形式で公式ページに掲載されています。

tleague.jp

ただこのランキングはシーズンを通して13試合以上の試合に出場しないと、集計対象外になってしまいます。(21試合 ☓ 0.6以上が条件のため)より集計対象を広げたランキングを見てみたいと思います。


また、サーブ得点率女子一位の石川選手と男子一位の張本選手には5%ほどの差があります。女子が全体的にサーブ得点率が高くなりやすいのか、それともただ石川選手が鬼強いだけなのかがよくわかりません。この辺りも検証したいと思います。

目次

  1. サーブ得点率ランキング
  2. レシーブ得点率ランキング
  3. 基本統計量
  4. グラフにしてみる

サーブ得点率ランキング

男子

対象選手はサーブ機会が200以上、つまりTリーグの出場前試合を通して200回以上サーブを出した選手としています。また、ダブルスは除外していますが、ヴィクトリーマッチは含まれています。

それぞれの列の説明は以下の通りです。
serve_point : サーブ得点数
serve : サーブ機会数
serve_winrate : サーブ時の得点率

f:id:ishigentech:20190310150432p:plain

公式ページのランキングと比較しても、サーブ得点数、サーブ機会数がほとんど一致しているので、データは正しいものとして進めていきます。

6試合にしか出場しておらず、公式ランキングでは集計対象外だった林選手が張本選手に次いで二位となっています。

女子

f:id:ishigentech:20190310151218p:plain

こちらも公式ページでは集計対象外だった早田選手が二位となっています。11試合に出場しているのでたまたま運が良かったわけではなく、実力通りの数値になっていると思います。

レシーブ得点率ランキング

公式ページには掲載されていませんが、レシーブ得点率も集計しました。

男子

サーブ同様、対象選手はレシーブ機会数が200以上の選手にします。

それぞれの列の説明は以下の通りです。
receive_point : レシーブ得点数
receive : レシーブ機会数
receive_winrate : レシーブ時の得点率

f:id:ishigentech:20190310151635p:plain

張本選手が一位なのはもうさすがとしか言いようがありませんが、サーブ時得点率ではそこまで上位ではなかった森園選手が二位となっているのが意外でした。ダブルスのレシーブだけでなく、シングルのレシーブ力もかなり高いことが窺えます。

女子

f:id:ishigentech:20190310152121p:plain

ツートップが離れているのはサーブ時の得点率と変わらずという結果になりました。トップと最下位の差がけっこう大きいなとも思います。

基本統計量

女子は差が大きかったような気がしたので、それぞれの基本統計量だけ見ておきます。

f:id:ishigentech:20190310152842p:plain

平均値にはほとんど差がありませんが、女子のほうが少しだけサーブが有利なのかもしれません。標準偏差は女子のほうが大きくなっており、ばらつきが大きい、つまり個々の選手の実力差が男子と比較すると大きいとは言えそうです。

グラフにしてみる

男女まとめ

f:id:ishigentech:20190310153420p:plain

女子のほうがばらばらになっていて、サーブ得点率とレシーブ得点率の相関関係が高そうに見えます。

男子

f:id:ishigentech:20190310153435p:plain

男子はサーブ得点率とレシーブ得点率は相関関係がほとんどありません。

森園選手のようにレシーブの得点率が高く、サーブの得点率はさほど高くない選手がいたり、陳選手のようにサーブの得点率が高く、レシーブの得点率はさほど高くない選手もいます。

女子

f:id:ishigentech:20190310153445p:plain

男子に比べラリー勝負になる展開が多く、ラリーに強い選手の両方の得点率が高いという構図になっていそうです。

わかったこと

  1. 石川選手が鬼強く、サーブ得点率が高くなっていた
  2. 早田選手も鬼強かった
  3. 公式ランキングでは集計対象外の選手も見てみると面白かった
  4. レシーブ得点率も公式ページに掲載してほしい

こんなところです。今回は以上です。今後はチームごとにやるとか、得点推移に着目するとか、気が向けばやってみます。

Tリーグの試合結果をスクレイピングで収集する

Tリーグのプレーオフもいよいよ来週に迫ってきました。今回はTリーグのこれまでの試合のデータをスクレイピングで収集してみました。pythonスクレイピングしてmysqlで作成したDBに突っ込むまでやってみます。

なぜこんなことをやるのかというと、もっと興味深いデータが眠っていると思ったからです。今でもランキング形式で多少のデータが載っていますが、もっと細かくできるんじゃないかと。API用意しろとか贅沢は言わないので、このくらいのアクセスは許して欲しいところです。

追記 2019-03-24

選手のデータを収集するプログラムもgithubに追加しました。

環境

言語 Python

説明するまでもなく。BeautifulSoup,requestsライブラリとかも同様に。

データベース mysql

dockerですぐに使えそうだったのでmysqlにしました。INSERT文でデータを作成するだけなのでどのデータベースでも同じように動くはずです。

実装

github.com

ここに置いています。収集したデータを配布する行為は会員規約での禁止行為*1なので、ソースコードだけ置いておきます。一回動かしても100回弱くらいのアクセスなので、サーバーに高い負荷をかけることはないと考えています。とはいえ頻繁に動かすのもどうかとは思うので、自己責任で動かしてください。

解説

最終的には以下のような3つのテーブルにデータを突っ込むイメージです。

team_match

チームでの対戦成績です。
ホームチーム、アウェイチーム、ホームチームの団体でのポイント、アウェーチームの団体でのポイント、来場者数、性別が入ります。

game

各試合での対戦成績です。
ホームチームの選手、アウェーチームの選手、ホームチームの選手の獲得セット数、アウェーチームの選手の獲得セット数が入ります。

point

各ラリーでのポイントです。
セット数、ポイント数、ポイントの得失、サーブ権、ホームチームタイムアウト直後か、アウェーチームがタイムアウト直後かどうかが入ります。


まずは以下のSQLを実行して、データベースとテーブルを作成します。

CREATE DATABASE TLeagu;

CREATE TABLE team_match (
match_id VARCHAR(20) ,
date DATE ,
home VARCHAR(100) ,
away VARCHAR(100) ,
home_point INT ,
away_point INT ,
visitors INT ,
sex INT ,
PRIMARY KEY(match_id)
);

CREATE TABLE game (
match_id VARCHAR(20) ,
game_id INT ,
home_player1 VARCHAR(100) ,
home_player2 VARCHAR(100) ,
away_player1 VARCHAR(100) ,
away_player2 VARCHAR(100) ,
home_point INT ,
away_point INT ,
PRIMARY KEY(match_id,game_id)
);

CREATE TABLE point (
match_id VARCHAR(20) ,
game_id INT ,
set_id INT ,
point_id INT ,
point INT ,
serve INT ,
home_timeout_fag INT ,
away_timeout_fag INT ,
PRIMARY KEY(match_id,game_id,set_id,point_id)
);


次に必要な関数を定義していきます。

get_link

各試合へのリンクを配列にして取得します。

get_match_recorde

team_matchテーブルに対応したデータを取得します。

get_game_recorde

gameテーブルに対応したデータを取得します。

get_point_record

pointテーブルに対応したデータを取得します。

judge_end_set

ポイントの得失点からセットが終了したかどうかを判定します。

# func.py
import requests
from bs4 import BeautifulSoup
import re

basicurl = "https://tleague.jp/match/"

def get_link():
  link_list = []
  for month in ["201810","201811","201812","201901","201902"]:
    url = basicurl + "?season=2018&month=" + month + "&mw="
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser")
    matchlist = soup.find(class_="ui-match-table")
    for i in matchlist.find_all(class_="match-men"):
      for inner in i.find_all(class_="inner"):
        link = inner.find("a").get("href")
        link_list.append(link)
    for i in matchlist.find_all(class_="match-women"):
      for inner in i.find_all(class_="inner"):
        link = inner.find("a").get("href")
        link_list.append(link)

  return link_list


def get_match_recorde(soup):
  match = soup.find(class_="match-info")
  home = match.find(class_="home").get_text()
  away = match.find(class_="away").get_text()
  point = match.find_all(class_="cell-score")
  home_point = point[0].get_text()
  away_point = point[1].get_text()
  itemclass = match.find(class_="item-spec").find_all("li")
  visitors = itemclass[2].get_text()
  visitors = re.sub(r"[^0-9]", "", visitors)

  return [home, away,home_point, away_point, visitors]


def get_game_recorde(soup):
  table = []
  
  match = soup.find(class_="cell-game")
  home_point = 0
  away_point = 0
  game = []
  for i, col in enumerate(match.find_all(class_="col")):
    if i % 3 == 1:
        # point col
        home_point = col.get_text()[0]
        away_point = col.get_text()[2]
        continue
    a_list = col.find_all("a")
    if len(a_list) >= 2:
        for a in a_list:
            game.append(a.get_text().replace("\n",""))
    else:
        for a in a_list:
            game.append(a.get_text().replace("\n",""))
            game.append(None)
    if i % 3 == 2:
      # reset
      game.append(home_point)
      game.append(away_point)
      table.append(game)
      game = []

  return table

def judge_end_set(point, is_final):
  home = 0
  away = 0
  if is_final:
    home = 6
    away = 6
  for i in point:
    if i == "1":
      home += 1
    else:
      away += 1
      
    if home >= 11 or away >= 11:
      if (home - away) ** 2 >= 4:
        return True
      
  return False

def get_point_record(soup):
  match = []
  match_serve = []
  match_timeout = []
  home_timeout, away_timeout = 0, 0
  is_final = False
  for i in soup.find_all("div",class_="match-game"):
    game = []
    game_serve = []
    game_timeout = []
    for w in i.find_all(class_="wrap-table"):
      point = []
      serve = []
      timeout = []
      tmp = ""
      for k_ind,k in enumerate(w.find_all("td")):
        if k_ind == 0:
          if k.get_text() == "1" or k.get_text() == "0":
            point.append(k.get_text())
          else:
            point.append(str(int(k.get_text()) - 6))
            is_final = True
        else:
          if k.get_text() == "T":
              home_timeout = 1
              continue
          elif re.match("[0-9]", k.get_text()) == None:
              away_timeout = 1
              continue
          elif tmp == k.get_text():
            point.append("0")
          else:
            point.append("1")
        timeout.append([home_timeout, away_timeout])
        home_timeout, away_timeout = 0, 0
        tmp = k.get_text()
        if k.get("class") != None:
          serve.append(1)
        else:
          serve.append(2)

        if judge_end_set(point, is_final):
          is_final = False
          game.append(point)
          game_serve.append(serve)
          game_timeout.append(timeout)
          break
    match.append(game)
    match_timeout.append(game_timeout)
    match_serve.append(game_serve)

  return match, match_timeout, match_serve


そして実際にwebページにアクセスして実行します。処理は以下のとおりです。

  1. mysqlへのコネクションを作成
  2. func.pyで定義した関数を順番に実行
  3. 結果からINSERT文を作成
  4. DBに登録
# Tleagu.py
import requests
from bs4 import BeautifulSoup
import re
import mysql.connector
from func import get_game_recorde,get_link,get_match_recorde,get_point_record

conn = mysql.connector.connect(
    host = '172.17.0.3',#localhost
    port = 3306,
    user = '',
    password = '',
    database = 'TLeagu',
)

cur = conn.cursor()

basicurl = "https://tleague.jp"
link = get_link()
# link = ["/match/20181024m01"]
try:
    cur.execute("DELETE FROM team_match;")
    cur.execute("DELETE FROM game;")
    cur.execute("DELETE FROM point;")
    for i in link:
        url = basicurl + i
        print(url)
        response = requests.get(url)
        soup = BeautifulSoup(response.text, "html.parser")
        sex = 0 if i[-3] == "m" else 1
        date = i[-11:-3]
        match_id = i[7:]
        all_match = get_match_recorde(soup)
        all_match_game = get_game_recorde(soup)
        all_match_point, all_match_timeout, all_match_serve = get_point_record(soup)
        
        # inesrt match table
        execute_str = "INSERT INTO team_match VALUES ('{}','{}','{}','{}',{},{},{},{});".format(
            match_id, date, all_match[0], all_match[1], int(all_match[2]), int(all_match[3]), int(all_match[4]), int(sex)
        )
        cur.execute(execute_str)

        # insert game table
        for a_game_index, a_game in enumerate(all_match_game):
            if a_game[1] != None:
                execute_str = "INSERT INTO game VALUES ('{}',{},'{}','{}','{}','{}',{},{});".format(
                    match_id, a_game_index, a_game[0], a_game[1], a_game[2], a_game[3], int(a_game[4]), int(a_game[5])
                )
            else:
                execute_str = "INSERT INTO game VALUES ('{}',{},'{}',NULL,'{}',NULL,{},{});".format(
                    match_id, a_game_index, a_game[0], a_game[2], int(a_game[4]), int(a_game[5])
                )
            cur.execute(execute_str)

        # insert point table
        for a_match_point_index, a_match_point in enumerate(all_match_point):
            for a_set_point_index, a_set_point in enumerate(a_match_point):
                for a_point_index, a_point in enumerate(a_set_point):
                    execute_str = "INSERT INTO point VALUES ('{}',{},{},{},{},{},{},{});".format(
                        match_id, a_match_point_index, a_set_point_index, a_point_index,
                        int(a_point), int(all_match_serve[a_match_point_index][a_set_point_index][a_point_index]),
                        all_match_timeout[a_match_point_index][a_set_point_index][a_point_index][0],
                        all_match_timeout[a_match_point_index][a_set_point_index][a_point_index][1]
                    )
                    cur.execute(execute_str)
except:
    print(all_match)
    print(all_match_game)
    print(all_match_point)
    conn.rollback()
    raise

conn.commit()


ここまで実行すると、DBにデータが作成されていることが確認できると思います。

次回はこの取得したデータを使って分析してみたいと思います。

*1:https://fan.tleague.jp/terms/ 第4章 第10条 1.に該当

とあるwebサイトを作った話

ただの宣伝記事です。

こんなサイトを作ってみました。

www.datapingpong.com

使い方は説明するまでもなく、得失点の内容に応じてボタンをぽちぽちするだけです。

まだまだできることは限られていますが、昔の自分なら使うかどうかを考えた結果、作ってみることにしました。

当時は似たようなことを紙で集計していたので、それに比べればかなり使いやすくなったと自己満足しています。


特に説明することもないので、ありがちな質問に回答しておきます。

・こんなの意味あるの?
ただ集計しているだけなので、使うだけでは意味はありません。意味を持たせるのは使う人次第だと思います。何かとデータを見るのが好きで、卓球にそれなりに時間をかけている人なら、意味はあるんじゃないかと。

・誰も使わないのでは?
わざわざ自分で試合の反省をし、かつそれを数値で表したい人なんてほとんどいないでしょう。現段階では日本で10人の役に立てばいいかなと思っています。

・何を目指しているの?
『さいきょうのじんこうちのう』ってやつが欲しいです。人間が獲得できる経験値を越えたその先に何があるのか見てみたいわけです。


ちなみに現在は人工知能だなんてものとはほど遠く簡単な処理しかしていません。実現できそうなものから順にやってけばいいかなと。いずれはもっと多くの人に役立つものにしていきます。飽きない限りもっともっと面白くなるので、たまには見てみてください。

yolov3を自前データで学習(成功編)

今回は前回失敗したままだった、yolov3の自前データでの学習についてです。

ishigentech.hatenadiary.jp

結論としては学習するところまではいけました。ただ、期待していたような結果は出ませんでした。RTX2080を購入したので、とりあえず学習までできたのは良かったかなと思います。

環境

ubuntu16.04
Geforce RTX 2080
nvidia-docker
opencv3.3.0

使用した実装はこちら

pjreddie.com
前回はkerasでの実装でしたが、cndnnのバージョンがシビアだったので変更しました。

手順

  1. nvidia-dockerでコンテナ作成
  2. opencv、darknetのインストール
  3. VOTTでアノテーションを行い、学習用データを作成(今回は100枚ほど)
  4. 学習を行う

nvidia-dockerでコンテナ作成

いい感じのDockerfile作れよって話かもしれませんが、やっていません。すみません。

イメージをダウンロードします。

docker pull nvidia/cuda:9.0-cudnn7-devel

コンテナの立ち上げです。
docker内でopencvGUIアプリケーションとして動かすため、環境変数やマウントしなければならないディレクトリがあります。また、ホストのxhostの設定も変更する必要があります。

xhost +local:root
nvidia-docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -it イメージ /bin/bash

また、マウントしたいディレクトリが他にもあれば-vオプションを追加します。

nvidia-docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -v ホスト:コンテナ -it イメージ /bin/bash

opencv、darknetのインストール

ここは割愛します。以下参考記事の通りです。
Ubuntu16.04へのOpenCV 3.3.0インストールメモ - Qiita
YOLO: Real-Time Object Detection
darknetでYOLOv3を動かしてみた。 - Qiita

VOTTでのアノテーションは過去の記事の通りです。この時に作成したデータを使用しました。

ishigentech.hatenadiary.jp

学習を行う

学習済みのデータをダウンロードします。

wget https://pjreddie.com/media/files/darknet53.conv.74

環境によりますが、obj.dataやここで参照している各ファイルのパスは適宜合わせてください。
学習は以下のコマンドで行います。

./darknet detector train sample/data/obj.data sample/yolo-obj.cfg darknet53.conv.74

f:id:ishigentech:20190106143705p:plain
学習中のターミナル

学習が始まるとdarknet/backupに学習済みデータが保存されていくので、学習途中でも以下のコマンドで精度を確認することができます。

./darknet detector test sample/data/obj.data sample/yolo-obj.cfg backup/sample.backup テストデータ

手順は以上です。

今回は卓球の選手とボールの教師用データを作成し、学習させてみました。結果としては選手は検出でき、ボールはできていないという結果になりました。小さな物体に対してはまだ難しいのだろうと思います。

f:id:ishigentech:20190106144037p:plain
予測結果

ボールのトラッキングにはまだ時間がかかりそうでした。

yolov3を自前データで学習(失敗編)

今回はyolov3を自前のデータで学習させて、動かしてみます。学習させるのは動画のデータです。

環境

ubuntu 16.04
python 3.6
keras
Geforce GTX 1050

こちらのkeras実装を使わせていただきました。正直なところ、VOTTを利用して教師データを作成しYOLOで学習させるのであれば、別の実装を使ったほうがいいとは思います。

github.com

参考記事

qiita.com


また、アノテーションにはVOTTを使いました。

ishigentech.hatenadiary.jp

このモデルの学習用のデータフォーマットと出力されるファイル形式が異なるので、モデルのフォーマットに合わせたテキストデータを作成します。このモデルの学習用のフォーマットは

画像のパス min_x,min_y,max_x,max_y,クラス番号 min_x,min_y,max_x,max_y,クラス番号
画像のパス ~

上記のように一つのテキストファイルに全ての画像のパス、ボックスの位置、クラス番号を記載します。訓練用と検証用のデータは分けずに記載して構いません。train.py実行時に分けてくれるみたいです。

以下のソースでtrain.txtという名前の訓練用データを作成しました。

WIDTH = 640
HEIGHT = 360
txtpath = '...../data/obj/'
imgpath = '...../data/obj/'
w = open('train.txt','w')
w.close()
#ファイル数に合わせてループさせる
for i in range(1, 103):
    txtfilename = '....._frame_'+str(i)+'.txt'
    imgfilename = '....._frame_'+str(i)+'.jpg'
    txt = imgpath+imgfilename
    f = open(txtpath+txtfilename)
    boxes = f.readlines()
    f.close()
    for box in boxes:
        boxElementList = box.split()
        boxElementList = [float(n) for n in boxElementList]
        for i, boxElement in enumerate(boxElementList):
            if i == 0: continue
            if i == 1:
                x_mean = boxElement * WIDTH
            if i == 2:
                y_mean = boxElement * HEIGHT
            if i == 3:
                x_width = boxElement * WIDTH / 2
            if i == 4:
                y_height = boxElement * HEIGHT / 2
            #print(boxElement)
        x_min = round(x_mean - x_width)
        y_min = round(y_mean - y_height)
        x_max = round(x_mean + x_width)
        y_max = round(y_mean + y_height)
        txt += ' '+str(x_min)+','+str(y_min)+','+str(x_max)+','+str(y_max)+','
        txt += str(int(boxElementList[0]))
    txt += '\n'
    #print(txt)
    w = open('train.txt','a')
    w.write(txt)
    w.close()

batch_sizeはとりあえず1でやってみます。

python train.py

これで学習できると思っていると、私のマシンに搭載されているGeforce GTX 1050(2G)だと落ちます。
はじめの50エポックまではいけることもありますが、50エポックから学習する層の数が増えるので、間違いなく落ちます。

f:id:ishigentech:20181006211133p:plain

遅いかもしれないけど、学習はできると思っていたので、けっこうショックです。。。
AWSでやってみるか、1080Tiとかを頑張って買うかしないようです。

環境を変えてまた再チャレンジします。

VOTTを使ってみる

今回はVOTTを使ってアノテーションしてみます。方法についてはgithubのページにも記載があります。ここでは簡単な手順と実際に動かしみて感じたことを書きます。

以下のソースからご自身のマシンに対応するファイルをダウンロードして、解凍すると使えるはずです。

github.com

起動後の画面からyoutubeっぽいマークのアイコンを選び、動画ファイルを選択すると動画のアノテーションになります。注意して頂きたいのは、この時点でファイル名に空白があったり、全角文字が含まれていると、学習するときに面倒なことになる可能性もあるので、変更しておくことをお勧めします。

f:id:ishigentech:20181006190338p:plain

開くとこんな画面になります。今回は下記画像の設定で行います。

f:id:ishigentech:20181004204303p:plain

設定項目は以下の通りです。

  • Frame Extraction Rate

アノテーションを行うfpsを指定します
1だと1フレームずつアノテーションになります

  • Region Type

ボックスの種類を選択します

  • Suggested Region Method

次のフレームのボックスをどうするかの設定です

  • Labels* (Comma Seperated)

カンマ区切りで付与したいラベルをすべて入力します


ここからはドラッグアンドドロップで枠を作成し、ラベルを選択します。数千とかやってると地獄の作業です。

f:id:ishigentech:20181004204323p:plain

私の環境だと次のフレームに移動すると、ボックスが少しずつずれます。そのような場合は画像のサイズにウィンドウを合わせると、若干改善されます。

作りたいフレーム分だけ作業が終わったら、
Object DettectionのExport Tagsを選択します。
すると下記の画面が表示されます。

f:id:ishigentech:20181004205223p:plain

今回はこの画面の設定で出力します。量が多いとそれなりに時間がかかります。

f:id:ishigentech:20181004210121p:plain

出力されるディレクトリ構成はこうなります。設定ファイルとdataディレクトリがあって、dataディレクトリの中にクラス名とパスが記載されたファイルができます。そしてobjディレクトリにフレームごとに画像化したものと、フレームごとのボックスの位置とクラス名が記述されたファイルが出力されます。ファイル名は0埋めしてほしいところでした。

クラス名とボックスの位置が記載されたtxtファイルはYOLOのフォーマットである

クラス番号 x中心座標 y中心座標 幅 高さ
ボックスの数だけ記載

このようなファイル形式で出力されます。

モデルによってはこのまま学習させることもできます。

VOTTを紹介している記事があまりなかったため、書いてみました。次はこのデータを使って学習させてみたいと思います。

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

今回は前回の続きという形です。

ishigentech.hatenadiary.jp


前回は可視化する部分でグラフにプロットしていましたが、わかりにくいと思うので、GIF動画を作ってみます。

下記ページを参考にさせていただきました。
stmind.hatenablog.com

作成した動画

7つのスイングを重ねてGIF動画にしています。

コードについて


importと後々使う変数の定義

import json
import numpy as np
import matplotlib.pyplot as plt
import os
from pylab import *
from PIL import Image, ImageDraw
import numpy as np
import matplotlib.pyplot as plt

#全表示
#pairList = np.array([0,1,1,2,2,3,3,4,1,5,5,6,6,7,1,8,8,9,9,10,1,11,11,12,12,13,0,14,14,16,0,15,15,17])
#目と耳削除
pairList = np.array([0,1,1,2,2,3,3,4,1,5,5,6,6,7,1,8,8,9,9,10,1,11,11,12,12,13])
pairList = pairList.reshape(int(len(pairList) / 2), 2)
index = np.array([139,208,277,343,415,487,558])
colors = [(255.,     0.,    85.),
          (255.,     0.,     0.),
          (255.,    85.,     0.), 
          (255.,   170.,     0.), 
          (255.,   255.,     0.), 
          (170.,   255.,     0.), 
          (85.,   255.,     0.), 
          (0.,   255.,     0.), 
          (0.,   255.,    85.), 
          (0.,   255.,   170.), 
          (0.,   255.,   255.), 
          (0.,   170.,   255.), 
          (0.,    85.,   255.), 
          (0.,     0.,   255.), 
          (255.,     0.,   170.), 
          (170.,     0.,   255.), 
          (255.,     0.,   255.), 
          (85.,     0.,   255.)]

ファイルを開いて変数に格納

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)

GIF動画を作成する関数の定義

def generateGIF(swing, path):
    images = []
    xlim = int(np.max(swing[:, :, :, 0]))
    ylim = int(np.max(swing[:, :, :, 1]))
    for i in range(len(swing[0, :, :, :])):
        im = Image.new('RGB', (xlim, ylim))#, (128, 128, 128))
        draw = ImageDraw.Draw(im)
        for d in swing:
            for pair in pairList:
                pt1 = (int(d[i, pair[0], 0]), int(d[i, pair[0], 1]))
                c1 = d[i, pair[0], 2]
                pt2 = (int(d[i, pair[1], 0]), int(d[i, pair[1], 1]))
                c2 = d[i, pair[1], 2]
                # 信頼度0.0の関節は無視
                if c1 == 0.0 or c2 == 0.0:
                    continue
                # 関節の描画
                color = tuple(list(map(int, colors[pair[0]])))        
                draw.line((pt1, pt2), fill=color, width=2)
                for line in range(1, 3):
                    draw.line((0,int(ylim/3)*line, xlim, int(ylim/3)*line), fill=(50,50,50), width=1)
                for line in range(1, 3):
                    draw.line((int(xlim/3)*line, 0, int(xlim/3)*line, ylim), fill=(50,50,50), width=1)
        images.append(im)
    images[0].save(path,
                   save_all=True, append_images=images[1:], optimize=False, duration=500, loop=0)

全体のデータからスイング部分を切り出し、両骨盤の平均位置を原点に調整して、GIF動画を作成

swingperframe = 20
swing = np.array([])
for i in index:
    swing = np.append(swing, data[i:i+swingperframe,:,:])
swing = swing.reshape(len(index),swingperframe, 18, 3)
    
for i, ivalue in enumerate(swing):
    for k, kvalue in enumerate(ivalue):
        originx = np.mean([kvalue[8, 0], kvalue[11, 0]])
        originy = np.mean([kvalue[8, 1],kvalue[11, 1]])
        swing[i, k, :, 0] -= originx
        swing[i, k, :, 1] -= originy
        for t, tvalue in enumerate(kvalue):
            if tvalue[2] == 0:
                swing[i, k, t, 0] = 0
                swing[i, k, t, 1] = 0
        
swing[:, :, :, 0] -= np.min(swing[:, :, :, 0])
swing[:, :, :, 1] -= np.min(swing[:, :, :, 1])

generateGIF(swing, 'ファイル名.gif')


以上です。やってることは簡単なことしかやっていませんが、グラフなんかよりは見やすくなったと思います。