
バドミントン試合解析アプリ実装してみた!その①
こんにちは。
ポンコツ教員ブログです。
今回は、前回作成した試合解析アプリを
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 問い合わせ
データ解析やりたいけどできない、やり方がわからない、もっとこうした方がいいなどのご意見あれば、修正・対応いたしますので、たくさん意見を頂ければと思います!!