見出し画像

バドミントン試合解析アプリ実装してみた!その①

こんにちは。
ポンコツ教員ブログです。
今回は、前回作成した試合解析アプリを
Youtubeにある国際大会の動画を引用し、動かしてみました。


1 使い方

<ボタンの説明>
1:コート上にある配球ボタン
 →打った場所にボタンを押すと軌跡が出る
2:ラリー終了ボタン
 →最後ミスした場所まで配球ボタンを押し終えたら、配球の線を消し、次
    のラリーに移行する
3:ゲーム切り替えボタン
 →1ゲームが終わったら押す。ボタンの配置が切り替わります。
4:リセットボタン
 →配球の場所を間違えたときに使用。現在のラリーの記入した軌跡が消え  
  ます
5:ミスランキング
 →現在までのミスの多い箇所をランキング形式で出します。
6:統計データ
 →最後にラリー終了ボタンが押されるまでのミスの統計をコート内に%で
       表示します
7:スコアボタン
 →押されるとスコアを表示する。
8:元に戻すボタン
 →配球ボタンで押した場所が間違ってしまったとき、その間違った線を消
    して、一つ前の位置からやり直せる。
9:一つ前にラリーに戻る
 →得点が間違ってしまったときに使う。間違って記録したラリーを消して
    0からやり直せる

2 使用するメリット

使用するメリットとしては
・自分の強み、弱点が視覚的に知れる。
・配球パターンが視覚的にわかるので、ショットの修正ができる。
・アプリにしたので、大会当日、liveでも使用可能

3 考察

0から自分で作り、動作させてみた感想としては、まだまだ不十分な出来だと感じた。だが、ミスした箇所を%で表すことができたのは、試合中でも生かせると実感できた。まだ試作段階だが、これからは、試合の解析データも記事にして、どんな解析データが取れたか表示していきたい

4 プログラム

長いプログラム嫌だなと思いますが、安心してください!
このこのプログラムをコピーして、画像の保存先だけ決めれば
誰のパソコンでも動作することはできます!
わからない場合や動作させたい場合は、問い合わせから質問していただければ、リモートで設定もします!

import tkinter as tk
from PIL import ImageGrab  # PILモジュールをインポート
from collections import Counter
import pyautogui
import pygetwindow as gw
from collections import Counter


# グローバル変数の初期化
final_positions = []  # 最後に押されたボタンの位置を保存するリスト
path_data = []
all_paths = []  # すべてのラリーの軌跡データを保存
click_count = 0  # クリックされたボタンの回数を追跡
rally_path_data = []


def on_button_click(coat, row, col, x, y, button_name):
    global click_count
    click_count += 1
    print(f"ボタンがクリックされました: コート={coat}, 行={row}, 列={col}")

    if len(path_data) == 0 or path_data[-1][2] != coat:
        path_canvas.create_oval(x - 5, y - 5, x + 5, y + 5, fill="yellow", tags=("oval" + str(click_count)))

    path_data.append((x, y, coat, button_name))

    if len(path_data) > 1:
        x1, y1, coat1, _ = path_data[-2]
        x2, y2, coat2, _ = path_data[-1]

        if coat2 == "ビジター":
            x1 += 5
            x2 += 5

        # coat1 と coat2 の値を考慮して色を設定します。
        if coat1 == home and coat2 == visitor:
            color = home_color
        elif coat1 == visitor and coat2 == home:
            color = visitor_color
        else:
            color = home_color if coat == home else visitor_color

        path_canvas.create_line(x1, y1, x2, y2, fill=color, width=2, arrow=tk.LAST, tags=("line" + str(click_count)))
        mid_x, mid_y = (x1 + x2) / 2, (y1 + y2) / 2
        offset = -10 if coat2 == "ホーム" else 10
        path_canvas.create_text(mid_x, mid_y + offset, text=str(click_count), fill="white", tags=("text" + str(click_count)))

def print_final_positions_stats():
    counter = Counter(str(pos) for pos in final_positions)
    for position, count in counter.items():
        print(f"{position}: {count} times")

# グローバル変数の初期化
game_number = 1
rally_count = 1  # ラリーカウントを追跡する変数
home_score = 0
score_frame = None
game_scores = []
visitor_score = 0
score_window = None
score_label = None
home = "ホーム"
visitor = "ビジター"
stats_window = None
stats_canvas = None
home_color = "red"
visitor_color = "blue"
game_states = []
rally_states = []

def update_score(button_name):
    global home_score, visitor_score, game_number

    # スコアボタンのリストを coat に基づいて更新
    scoring_buttons_home = [
        f"out{home}\n(1,1)", f"out{home}\n(1,2)", f"out{home}\n(1,3)", f"out{home}\n(1,4)", f"out{home}\n(1,5)",
        f"out{home}\n(2,1)", f"out{home}\n(3,1)", f"out{home}\n(4,1)", f"out{home}\n(2,5)", f"out{home}\n(3,5)", f"out{home}\n(4,5)",
        f"{visitor}\n(1,2)", f"{visitor}\n(1,3)", f"{visitor}\n(1,4)", f"{visitor}\n(2,2)", f"{visitor}\n(2,3)", f"{visitor}\n(2,4)",
        f"{visitor}\n(3,2)", f"{visitor}\n(3,3)", f"{visitor}\n(3,4)", f"{visitor}\n(4,2)", f"{visitor}\n(4,3)", f"{visitor}\n(4,4)"
    ]

    # 偶数のゲーム数の場合、スコアのロジックを逆にします
    if game_number % 2 == 0:
        if button_name in scoring_buttons_home:
            visitor_score += 1
        else:
            home_score += 1
    else:
        if button_name in scoring_buttons_home:
            home_score += 1
        else:
            visitor_score += 1


def show_score_in_gui():
    global score_window, score_label, score_frame
    score_text = f"ホーム: {home_score}  -  ビジター: {visitor_score}"

    if not score_window:
        # 新しいウィンドウを作成
        score_window = tk.Toplevel(root)
        score_window.title("スコア")
        score_window.protocol("WM_DELETE_WINDOW", close_score_window)

        score_frame = tk.Frame(score_window)
        score_frame.pack(padx=10, pady=10)

    # 現在のスコアを更新または作成
    if score_label:
        score_label.config(text=score_text)
    else:
        score_label = tk.Label(score_frame, text=score_text, font=("Arial", 16))
        score_label.pack(pady=5)

def switch_game():
    global game_number, home_score, visitor_score, score_label, final_positions
    global home_color, visitor_color

    final_positions = []  # この行を追加して、統計データをリセットします

    if not score_window:
        show_score_in_gui()

    # 前のゲームの最終スコアを表示
    final_score_text = f"ゲーム {game_number} の結果: ホーム {home_score} - ビジター {visitor_score}"
    final_score_label = tk.Label(score_frame, text=final_score_text, font=("Arial", 12))
    final_score_label.pack(pady=5)

    # スコアをリセット
    home_score = 0
    visitor_score = 0
    game_number += 1

    # スコアラベルを更新
    score_label.config(text=f"ホーム: {home_score}  -  ビジター: {visitor_score}")

    global rally_count
    rally_count = 1

    if game_number % 2 == 0:  # 偶数ゲームの場合
        home_color = "blue"
        visitor_color = "red"
    else:  # 奇数ゲームの場合
        home_color = "red"
        visitor_color = "blue"

    switch_sides()  # ホームとビジターの文字列を入れ替え、関連するボタンのテキストを更新

def switch_sides():
    global home, visitor
    home, visitor = visitor, home  # ホームとビジターの文字列を入れ替えます。

    # GUI上のボタンのテキストを更新
    for widget in canvas.winfo_children():
        widget.destroy()

    for i in range(4):
        for j in range(5):
            x_position = 15*1.1 + j * 76*1.1

            # ホームのボタンのテキストを取得
            y_position_home = 15*1.1 + i * 76*1.1
            button_text_home = get_button_text(home, i+1, j+1)
            button_home = tk.Button(root, text=button_text_home,
                                    command=lambda coat=home, row=i+1, col=j+1, x=x_position + 38, y=y_position_home + 38, button_name=button_text_home: on_button_click(
                                        coat, row, col, x, y, button_name))
            canvas.create_window(x_position, y_position_home, window=button_home, anchor='nw', width=75, height=70)

            # ビジターのボタンのテキストを取得
            y_position_visitor = 350*1.1 + i * 76*1.1
            button_text_visitor = get_button_text(visitor, i+1, j+1)
            button_visitor = tk.Button(root, text=button_text_visitor,
                                       command=lambda coat=visitor, row=i+1, col=j+1, x=x_position + 38, y=y_position_visitor + 38, button_name=button_text_visitor: on_button_click(
                                           coat, row, col, x, y, button_name))
            canvas.create_window(x_position, y_position_visitor, window=button_visitor, anchor='nw', width=75, height=70)

def close_stats_window():
    global stats_window, stats_canvas
    stats_canvas = None  # 追加
    stats_window = None

def close_score_window():
    global score_window, score_canvas
    score_canvas = None  # score_canvasをNoneにリセット
    score_window = None  # score_windowをNoneにリセット

def undo_last_path():
    global path_data, click_count
    if path_data:
        # 最後のエントリを削除
        path_data.pop()
        click_count -= 1

        # canvas上の最後の線、テキスト、およびovalを削除
        path_canvas.delete("line" + str(click_count + 1))
        path_canvas.delete("text" + str(click_count + 1))
        path_canvas.delete("oval" + str(click_count))

def undo_last_rally():
    global home_score, visitor_score, rally_count, path_data, click_count, all_paths, final_positions, path_canvas

    # 最後のラリーの状態を復元する
    if rally_states:
        home_score, visitor_score, rally_count, path_data, click_count, all_paths, final_positions = rally_states.pop()

        # canvas上の最後の線、テキスト、およびovalを削除
        for i in range(click_count, 0, -1):
            path_canvas.delete("line" + str(i))
            path_canvas.delete("text" + str(i))
            path_canvas.delete("oval" + str(i))

        # GUIのスコア表示を更新
        if score_label:
            score_text = f"ホーム: {home_score}  -  ビジター: {visitor_score}"
            score_label.config(text=score_text)

def undo_all_last_path():
    global home_score, visitor_score, rally_count, path_data, click_count, all_paths, final_positions

    # 軌跡データをリセット
    path_data = []
    click_count = 0
    path_canvas.delete("all")
    draw_court_lines()

    # 最後のラリーのデータを削除
    if all_paths:
        all_paths.pop()

    # 最後のスコア入力を取り消す
    if final_positions:
        last_position = final_positions.pop()

        if home in last_position:
            home_score -= 1
        else:
            visitor_score -= 1

    # ラリーカウントを減少
    if rally_count > 1:
        rally_count -= 1

    # GUIのスコア表示を更新
    if score_label:
        score_text = f"ホーム: {home_score}  -  ビジター: {visitor_score}"
        score_label.config(text=score_text)

    if game_states:
        home_score, visitor_score, rally_count, path_data, click_count, all_paths, final_positions = game_states.pop()

def end_rally():
    global path_data, click_count
    if path_data:
        x, y, coat, button_name = path_data[-1]
        final_positions.append(button_name)
        update_score(button_name)  # ここでスコアを更新
    all_paths.append(list(path_data))
    path_data = []
    click_count = 0
    save_canvas()
    path_canvas.delete("all")
    draw_court_lines()
    
    global rally_count
    rally_count += 1

    game_states.append((home_score, visitor_score, rally_count, list(path_data), click_count, list(all_paths), list(final_positions)))
    rally_states.append((home_score, visitor_score, rally_count, list(path_data), click_count, list(all_paths), list(final_positions)))

def display_current_scores():
    global score_frame, game_scores
    score_text = f"ホーム: {home_score}  -  ビジター: {visitor_score}"
    
    # 既存のスコア表示をクリア
    for widget in score_frame.winfo_children():
        widget.destroy()

    game_scores.append(tk.Label(score_frame, text=f"ゲーム {len(game_scores) + 1}: {score_text}", font=("Arial", 12)))
    game_scores[-1].pack(pady=5)


def save_canvas():
    global game_number, rally_count

    window = gw.getWindowsWithTitle('Path Window')[0]
    if window:
        x = window.left
        y = window.top
        width = window.width
        height = window.height

        # こちらのrally_countの増加を削除します
        save_path = fr"C:\Users"ここだけ自分で変えてください!"\game{game_number}_rally{rally_count}.png"
        screenshot = pyautogui.screenshot(region=(x, y, width, height))
        screenshot.save(save_path)
    else:
        print("ウィンドウが見つかりませんでした.")

def reset_canvas():
    global path_data, click_count
    path_data = []
    click_count = 0
    path_canvas.delete("all")
    draw_court_lines()

def draw_court_lines():
    path_canvas.create_line(0, 329*1.1, 400*1.1, 329*1.1, fill="white")
    
    x1 = (11 + 1 * 76)*1.1  # ボタン(2,2)の左上のx座標
    y1 = (11 + 1 * 76)*1.1  # ボタン(2,2)の左上のy座標
    x2 = (11 + 4 * 76 )*1.1 # ボタン(4,4)の右下のx座標
    y2 = (346 + 3 * 76)*1.1  # ビジターボタン(4,4)の右下のy座標
    path_canvas.create_rectangle(x1, y1, x2, y2, outline="white", width=2, fill="")  # 長方形を描画

def show_mistake_ranking():
    counter = Counter(final_positions)
    ranked_positions = counter.most_common()  # ミスの多い順にソート

    # ランキングを表示するための新しいウィンドウを作成
    ranking_window = tk.Toplevel(root)
    ranking_window.title("ミスランキング")

    for index, (position, count) in enumerate(ranked_positions, start=1):
        cleaned_position = position.replace("\n", " ")  # 改行をスペースに置き換える
        label = tk.Label(ranking_window, text=f"{index}. {cleaned_position}: {count} 回")
        label.pack(pady=5)

def show_stats_in_gui():
    global stats_canvas

    # stats_canvasがNoneの場合にのみ新しいウィンドウとcanvasを作成
    if stats_canvas is None:
        stats_window = tk.Toplevel(root)
        stats_window.title("統計データ")
        stats_window.geometry("440x880")
        stats_window.protocol("WM_DELETE_WINDOW", close_stats_window)  # 追加: ウィンドウが閉じられたときの処理
        stats_canvas = tk.Canvas(stats_window, width=400*1.1, height=680*1.1, bg="green")
        stats_canvas.pack(padx=0, pady=0)
    else:
        stats_canvas.delete("all")

    # 位置座標の定義
    positions_coords = {}
    for i in range(4):
        for j in range(5):
            for coat in [home, visitor]:  # この部分を変更
                button_text = get_button_text(coat, i+1, j+1)
                x_position = 15*1.1 + j * 76*1.1
                y_position = (15 if coat == home else 350)*1.1 + i * 76*1.1  # この部分を変更
                positions_coords[button_text] = (x_position + 38, y_position + 38)

    # 各座標でのクリック回数をカウント
    home_counter = Counter([position for position in final_positions if home in position])
    visitor_counter = Counter([position for position in final_positions if visitor in position])

    total_home_clicks = sum(home_counter.values())
    total_visitor_clicks = sum(visitor_counter.values())

    # ホームとビジターのエリアにそれぞれの割合を表示
    for position, count in home_counter.items():
        x, y = positions_coords.get(position, (0, 0))
        percent = (count / total_home_clicks) * 100 if total_home_clicks else 0
        color = "red" if home == "ホーム" else "blue"
        stats_canvas.create_text(x, y, text=f"{percent:.1f}%", fill=color, font=("Arial", 12))

    for position, count in visitor_counter.items():
        x, y = positions_coords.get(position, (0, 0))
        percent = (count / total_visitor_clicks) * 100 if total_visitor_clicks else 0
        color = "blue" if visitor == "ホーム" else "red"
        stats_canvas.create_text(x, y, text=f"{percent:.1f}%", fill=color, font=("Arial", 12))

    # コートの表示
    stats_canvas.create_line(0, 329*1.1, 400*1.1, 329*1.1, fill="white")
    x1 = (11 + 1 * 76)*1.1
    y1 = (11 + 1 * 76)*1.1
    x2 = (11 + 4 * 76 )*1.1
    y2 = (346 + 3 * 76)*1.1
    stats_canvas.create_rectangle(x1, y1, x2, y2, outline="white", width=2)
   
# ウィンドウの生成と設定
root = tk.Tk()
root.geometry("440x880")
canvas = tk.Canvas(root, width=400*1.1, height=680*1.1, bg="green")
canvas.pack(padx=0, pady=0)

# ホームのボタン
def get_button_text(coat, i, j):
    if coat == home:
        # coatが現在のホームの場合
        if (i, j) in [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (3, 1), (4, 1), (2, 5), (3, 5), (4, 5)]:
            return f"out{coat}\n({i},{j})"
        else:
            return f"{coat}\n({i},{j})"
    elif coat == visitor:
        # coatが現在のビジターの場合
        if (i, j) in [(1, 1), (1, 5),(2, 1) ,(2, 5),(3, 1), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5)]:
            return f"out{coat}\n({i},{j})"
        else:
            return f"{coat}\n({i},{j})"

# ホームのボタン
for i in range(4):
    for j in range(5):
        coat = "ホーム"
        x_position = 15*1.1 + j * 76*1.1
        y_position = 15*1.1 + i * 76*1.1
        button_text = get_button_text(coat, i+1, j+1)
        button = tk.Button(root, text=button_text,
                   command=lambda coat=coat, row=i+1, col=j+1, x=x_position + 38, y=y_position + 38, button_name=button_text: on_button_click(
                       coat, row, col, x, y, button_name))
        canvas.create_window(x_position, y_position, window=button, anchor='nw', width=75, height=70)
# ビジターのボタン
for i in range(4):
    for j in range(5):
        coat = "ビジター"
        x_position = 15*1.1 + j * 76*1.1
        y_position = 350*1.1 + i * 76*1.1
        button_text = get_button_text(coat, i+1, j+1)
        button = tk.Button(root, text=button_text,
                   command=lambda coat=coat, row=i+1, col=j+1, x=x_position + 38, y=y_position + 38, button_name=button_text: on_button_click(
                       coat, row, col, x, y, button_name))
        canvas.create_window(x_position, y_position, window=button, anchor='nw', width=75, height=70)
        
canvas.create_line(0, 329*1.1, 400*1.1, 329*1.1, fill="white")
x1 = (11 + 1 * 76)*1.1  # ボタン(2,2)の左上のx座標
y1 = (11 + 1 * 76)*1.1  # ボタン(2,2)の左上のy座標
x2 = (11 + 4 * 76 )*1.1 # ボタン(4,4)の右下のx座標
y2 = (346 + 3 * 76)*1.1  # ビジターボタン(4,4)の右下のy座標
canvas.create_rectangle(x1, y1, x2, y2, outline="white", width=2)  # 長方形を描画

path_window = tk.Toplevel(root)
path_window.title("Path Window")
path_window.geometry("440x770")
path_canvas = tk.Canvas(path_window, width=400*1.1, height=680*1.1, bg="green")
path_canvas.pack()
draw_court_lines()

# ラリー終了ボタンをrootウィンドウに配置
end_rally_button = tk.Button(root, text="ラリー終了", command=end_rally)
end_rally_button.place(x=20*1.1, y=680*1.1, width=100*1.1, height=40*1.1)  # ボタンの位置とサイズを調整


ranking_button = tk.Button(root, text="ミスランキング", command=show_mistake_ranking)
ranking_button.place(x=20*1.1, y=730*1.1, width=100*1.1, height=40*1.1)

switch_game_button = tk.Button(root, text="ゲーム切り替え", command=switch_game)
switch_game_button.place(x=150*1.1, y=680*1.1, width=100*1.1, height=40*1.1) # 適切な位置に配置

reset_button = tk.Button(root, text="リセット", command=reset_canvas)
reset_button.place(x=280*1.1, y=680*1.1, width=100*1.1, height=40*1.1)  # ボタンの位置とサイズを調整

stats_button = tk.Button(root, text="統計", command=show_stats_in_gui)
stats_button.place(x=150*1.1, y=730*1.1, width=100*1.1, height=40*1.1)  # ボタンの位置とサイズを調整

# 新しく追加する部分
score_button = tk.Button(root, text="スコア", command=show_score_in_gui)
score_button.place(x=280*1.1, y=730*1.1, width=100*1.1, height=40*1.1)

# "元に戻す"ボタン
undo_button = tk.Button(root, text="元に戻す", command=undo_last_path)
undo_button.place(x=150*1.1, y=780*1.1, width=100*1.1, height=40*1.1)

undo_rally_button = tk.Button(root, text="一つ前のラリーに戻る", command=undo_last_rally)
undo_rally_button.place(x=280*1.1, y=780*1.1, width=130*1.1, height=40*1.1)  # ボタンの位置とサイズを調整

root.mainloop()

5 問い合わせ

データ解析やりたいけどできない、やり方がわからない、もっとこうした方がいいなどのご意見あれば、修正・対応いたしますので、たくさん意見を頂ければと思います!!

この記事が参加している募集

私のスポーツ遍歴

この記事が気に入ったらサポートをしてみませんか?