board-recognizer / gui_app.py
wai572's picture
Merge commit '04b24527b550f02d4cac5bfb7b811a2e45b5f9aa'
4580319
import math
import os
import queue
import threading
import tkinter as tk
from ctypes import c_int, c_long, pointer, string_at
from datetime import datetime
from tkinter import filedialog, messagebox, ttk
import cv2
import numpy as np
from PIL import Image, ImageTk
# from transformers import pipeline
import dds
from identify_cards import (determine_and_correct_orientation, find_center_box,
find_rank_candidates, get_suit_from_color_rules,
get_suit_from_image_rules, load_suit_templates,
recognize_cards, save_img_with_rect)
from utils import (PrintTable, arrange_hand, convert2ddTableDeal, convert2dup,
convert2pbn, convert2pbn_board, convert2pbn_txt,
convert2xhd, convert_hands_to_binary_deal, is_text_valid,
reshape_table)
# --- グローバル変数・設定 ---
# print("TrOCRのAIモデルを読み込んでいます...(初回は数分かかります)")
# try:
# trocr_pipeline = pipeline(
# "image-to-text", model="microsoft/trocr-base-printed"
# )
# print("TrOCRの準備が完了しました。")
# except Exception as e:
# print(f"TrOCRモデルのロード中にエラー: {e}")
# trocr_pipeline = None
DEFAULT_THRESHOLDS = {
"L_black": 65.0,
"a_green": 126.0,
"a_black": 120.0, # 250.0, # 120.0,
# "a_black2": 126.0,
"a_red": 134.0,
"b_orange": 137.0,
"ba_green": 0.0,
"ba_black": -4.5,
"ab_black": 250.0,
"b_black": 121.3,
"b_black2": 132.5,
# "b_black2": 127.0,
"a_orange": 150.0,
"b_red": 145.0,
"a_b_red": 9.0,
}
SUITS_BY_COLOR = {
"black": "S",
"green": "C",
"red": "H",
"orange": "D",
"unknown": "*",
}
PLAYER_ORDER = ["north", "south", "west", "east"]
MARGIN = 200
SUIT_TEMPLATE_PATH = "templates/suits/"
def validate_deal(hands):
if not hands:
return False, "分析対象のカードデータがありません"
total_cards = []
# 手札が合計52枚あるかチェック
for player, hand in hands.items():
if len(hand) != 13:
return (
False,
f"エラー: {player.capitalize()}の手札が13枚ではありません",
)
for card in hand:
if card in total_cards:
return (
False,
f"エラー: 重複したカードが検出されました ({card})",
)
total_cards.append(card)
return True, "デックは正常です"
def load_image(path):
try:
# ファイルをバイナリモードで安全に読み込む
with open(path, "rb") as f:
# バイトデータをNumPy配列に変換
file_bytes = np.asarray(bytearray(f.read()), dtype=np.uint8)
# NumPy配列(メモリ上のデータ)から画像をデコード
image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
if image is None:
raise ValueError(
"OpenCVが画像をデコードできませんでした。ファイルが破損しているか、非対応の形式の可能性があります。"
)
return image, ""
# image_objects[filename] = image
except Exception as e:
# ファイル読み込み自体のエラーをキャッチ
# image_objects[filename] = None
return None, e
def get_player_region_image(image, box):
bx, by, bw, bh = box
h, w, _ = image.shape
player_regions = {
"north": image[0:by, :],
"south": image[by + bh : h, :],
"west": image[by - MARGIN : by + bh + MARGIN, 0:bx],
"east": image[by - MARGIN : by + bh + MARGIN, bx + bw : w],
}
# 向きの補正
for player, region in player_regions.items():
if region is not None and region.size > 0:
if player == "north":
player_regions[player] = cv2.rotate(region, cv2.ROTATE_180)
elif player == "east":
player_regions[player] = cv2.rotate(
region, cv2.ROTATE_90_CLOCKWISE
)
elif player == "west":
player_regions[player] = cv2.rotate(
region, cv2.ROTATE_90_COUNTERCLOCKWISE
)
return player_regions
def analyze_image_data(image_paths, progress_queue, trocr_pipeline):
all_results = []
num_total_files = len(image_paths)
progress_queue.put("テンプレート画像読み込み中...")
suit_templates = load_suit_templates(SUIT_TEMPLATE_PATH)
if not suit_templates:
raise ValueError(
f"エラー: {SUIT_TEMPLATE_PATH} フォルダにスートのテンプレート画像が見つかりません。"
)
try:
progress_queue.put("ステージ1/3: 文字候補を検出中...")
all_candidates_global = []
# image_objects = {}
for i, image_path in enumerate(image_paths):
filename = os.path.basename(image_path)
progress_queue.put(f"分析中 ({i+1}/{num_total_files}): {filename}")
image, error = load_image(image_path)
if image is None:
all_results.append(
{
"filename": filename,
"error": f"画像読み込みエラー: {error}",
}
)
# box = find_center_box(image)
print("detect board")
rotated_image, box, scale = determine_and_correct_orientation(
image, progress_queue.put
)
if box is None:
all_results.append(
{"filename": filename, "error": "中央ボードの検出に失敗"}
)
continue
print(box)
save_img_with_rect("debug_rotated.jpg", rotated_image, [box])
player_regions = get_player_region_image(rotated_image, box)
for player, region in player_regions.items():
candidates = find_rank_candidates(
region, suit_templates, player, scale
)
for cand in candidates:
cand["filename"] = filename
cand["player"] = player
all_candidates_global.append(cand)
progress_queue.put(
"ステージ2/3: 文字認識を実行中... (時間がかかります)"
)
if not all_candidates_global or not trocr_pipeline:
progress_queue.put("認識する文字候補がありませんでした。")
progress_queue.put(
all_results
) # エラーがあった画像の結果だけを返す
return
candidates_pil_images = [
Image.fromarray(cv2.cvtColor(c["img"], cv2.COLOR_BGR2RGB))
for c in all_candidates_global
]
ocr_results = trocr_pipeline(candidates_pil_images)
# --- ステージ3: 結果の仕分けと最終的なカードの特定 ---
progress_queue.put("ステージ3/3: 認識結果を仕分け中...")
# まず、ファイルごとに結果を格納する辞書を準備
temp_results = {
os.path.basename(p): {player: [] for player in PLAYER_ORDER}
for p in image_paths
}
print(temp_results)
print([result[0]["generated_text"] for result in ocr_results])
raw_data = []
blacks = []
reds = []
for i, result in enumerate(ocr_results):
text = result[0]["generated_text"].upper().strip()
print(text, is_text_valid(text))
text = is_text_valid(text)
if text is not None:
candidate_info = all_candidates_global[i]
print(
f"--- 診断中: ランク '{text}' of {candidate_info['player']} at {candidate_info['pos']} with thick:{candidate_info["thickness"]} ---"
)
color_name, avg_lab = get_suit_from_image_rules(
candidate_info["no_pad"], DEFAULT_THRESHOLDS
)
if color_name == "black" or color_name == "green":
blacks.append(
f"{color_name.ljust(6)}: a{avg_lab[1]:.2f},b{avg_lab[2]:.2f},a+b{avg_lab[1]+avg_lab[2]:.2f},a-b{avg_lab[1]-avg_lab[2]:.2f}"
)
if color_name == "red" or color_name == "orange":
reds.append(
f"{color_name.ljust(6)}: a{avg_lab[1]:.2f},b{avg_lab[2]:.2f},a+b{avg_lab[1]+avg_lab[2]:.2f},a-b{avg_lab[1]-avg_lab[2]:.2f}"
)
print(color_name)
if color_name == "mark":
continue
candidate_info["avg_lab"] = avg_lab
candidate_info["color"] = color_name
candidate_info["name"] = text
raw_data.append(candidate_info)
if color_name in SUITS_BY_COLOR:
suit = SUITS_BY_COLOR[color_name]
card_name = f"{suit}{text}"
filename = os.path.basename(candidate_info["filename"])
player = candidate_info["player"]
temp_results[filename][player].append(card_name)
print("\r\n".join(blacks))
# print("\r\n".join(reds))
progress_queue.put(raw_data)
except Exception as e:
progress_queue.put(f"致命的なエラー: {e}")
def convert2txt(all_result, title):
res = ""
for result in all_result:
res += "=" * 40 + "\n"
res += f"{title}\n"
res += "=" * 40 + "\n"
if "error" in result:
res += f" エラー: {result['error']}\n"
elif "hands" in result:
for player in PLAYER_ORDER:
hand = result["hands"].get(player, [])
res += f" {player.capitalize()}: {', '.join(hand) if hand else '(なし)'}\n"
res += "\n"
return res
def parse_hand_string(hand):
hand_list = hand.replace(" ", "").split(",")
return hand_list
XHD = "xhdファイル"
DUP = "dupファイル"
PBN = "pbnファイル"
# --- Tkinter GUI アプリケーションクラス ---
class CardRecognizerApp:
def __init__(self, root):
self.root = root
self.root.title("トランプカード認識アプリ")
self.root.geometry("800x600")
self.filepaths = []
self.all_result = []
self.thread = None
self.q = queue.Queue()
self.format_var = tk.StringVar()
self.raw_rank_data = []
self.trocr_pipeline = None
self.model_loading_thread = None
self.model_loaded = threading.Event()
self.model_load_error = None
self.selected_deal_var = tk.StringVar()
self.setup_ui()
self.start_model_loading()
def setup_ui(self):
# スタイル
style = ttk.Style()
style.configure(
"TButton", padding=6, relief="flat", font=("Yu Gothic UI", 10)
)
style.configure("TLabel", padding=5, font=("Yu Gothic UI", 10))
style.configure("Header.TLabel", font=("Yu Gothic UI", 14, "bold"))
# メインフレーム
main_frame = ttk.Frame(root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
top_frame = ttk.Frame(main_frame)
top_frame.pack(fill=tk.X, pady=10)
# ファイル選択部分
file_frame = ttk.Frame(main_frame)
file_frame.pack(fill=tk.X, pady=10)
self.select_button = ttk.Button(
file_frame, text="画像ファイルを選択", command=self.select_files
)
self.select_button.pack(side=tk.LEFT, padx=5)
self.filepath_label = ttk.Label(
file_frame, text="ファイルが選択されていません", anchor=tk.W
)
self.filepath_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
nav_frame = ttk.Frame(main_frame)
nav_frame.pack(fill=tk.X, pady=5)
ttk.Label(nav_frame, text="表示中のディール:").pack(
side=tk.LEFT, padx=(0, 5)
)
self.deal_selector_combo = ttk.Combobox(
nav_frame,
textvariable=self.selected_deal_var,
state="disabled",
width=50,
)
self.deal_selector_combo.pack(side=tk.LEFT)
self.deal_selector_combo.bind(
"<<ComboboxSelected>>", self.on_deal_selected
)
# 分析開始ボタン
self.analyze_button = ttk.Button(
main_frame,
text="分析開始",
command=self.start_analysis_thread,
state=tk.DISABLED,
)
self.analyze_button.pack(pady=10, fill=tk.X)
self.export_button = ttk.Button(
top_frame,
text="結果を出力",
command=self.export_results,
state=tk.DISABLED,
)
self.export_button.pack(side=tk.LEFT, padx=5)
self.dds_single_button = ttk.Button(
top_frame,
text="現在のボードをDDS分析",
command=self.start_dds_single_analysis,
state=tk.DISABLED,
)
self.dds_single_button.pack(side=tk.LEFT, padx=(15, 5))
self.dds_button = ttk.Button(
top_frame,
text="全てのファイルをDDS分析",
command=self.start_dds_analysis,
state=tk.DISABLED,
)
self.dds_button.pack(side=tk.LEFT, padx=5)
self.debugger_button = ttk.Button(
top_frame,
text="カラーデバッガー",
command=self.open_debugger,
state=tk.DISABLED,
)
self.debugger_button.pack(side=tk.LEFT, padx=5)
# 結果表示部分
results_frame = ttk.Frame(main_frame, padding="10")
results_frame.pack(fill=tk.BOTH, expand=True, pady=10)
results_frame.columnconfigure(1, weight=1)
self.north_hand_var = tk.StringVar(results_frame)
self.south_hand_var = tk.StringVar(results_frame)
self.west_hand_var = tk.StringVar(results_frame)
self.east_hand_var = tk.StringVar(results_frame)
self.player_vars = {
"north": self.north_hand_var,
"south": self.south_hand_var,
"west": self.west_hand_var,
"east": self.east_hand_var,
}
self.result_entries = {}
ttk.Label(
results_frame, text="最終分析結果:", style="Header.TLabel"
).grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=10)
for i, player in enumerate(PLAYER_ORDER, 1):
ttk.Label(
results_frame,
text=f"{player.capitalize()}:",
style="Header.TLabel",
).grid(row=i, column=0, sticky=tk.NW, padx=5, pady=5)
entry = ttk.Entry(
results_frame, textvariable=self.player_vars[player]
)
entry.grid(row=i, column=1, sticky=tk.EW, padx=5, pady=5)
self.result_entries[player] = entry
# self.result_labels[player] = ttk.Label(
# results_frame,
# text="-",
# wraplength=550,
# anchor=tk.W,
# justify=tk.LEFT,
# )
# self.result_labels[player].grid(
# row=i, column=1, sticky=tk.NW, padx=5, pady=5
# )
# ステータスバー
self.status_var = tk.StringVar()
self.status_var.set("AIモデルを初期化中...")
status_bar = ttk.Label(
root,
textvariable=self.status_var,
relief=tk.SUNKEN,
anchor=tk.W,
padding=5,
)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def start_model_loading(self):
self.model_loading_thread = threading.Thread(
target=self.initialize_model
)
self.model_loading_thread.demon = True
self.model_loading_thread.start()
self.root.after(100, self.check_model_status)
def initialize_model(self):
"""バックグラウンドで実行されるAIモデル読み込み処理"""
try:
print("TrOCRのAIモデルを読み込んでいます...")
self.trocr_pipeline = pipeline(
"image-to-text", model="microsoft/trocr-small-printed"
)
print("TrOCRの準備が完了しました。")
except Exception as e:
self.model_load_error = e
finally:
self.model_loaded.set() # 読み込み完了(成功または失敗)を通知
def check_model_status(self):
"""モデルの読み込みが完了したか定期的にチェックする"""
if not self.model_loaded.is_set():
# まだロード中なら100ms後にもう一度チェック
self.root.after(100, self.check_model_status)
return
if self.model_load_error:
# エラーが発生した場合
messagebox.showerror(
"起動エラー",
f"TrOCRモデルの読み込みに失敗しました。\nエラー: {self.model_load_error}",
)
self.status_var.set("AIモデルの読み込みに失敗しました。")
else:
# 成功した場合
self.status_var.set("準備完了")
self.filepath_label.config(text="ファイルが選択されていません")
if len(self.filepaths) > 0:
self.analyze_button.config(state=tk.NORMAL) # ボタンを有効化
def select_files(self):
self.filepaths = filedialog.askopenfilenames(
title="画像ファイルを選択 (複数選択可)",
filetypes=(
("JPEGファイル", "*.jpg;*.jpeg"),
("PNGファイル", "*.png"),
("すべてのファイル", "*.*"),
),
)
if self.filepaths:
if len(self.filepaths) == 1:
self.filepath_label.config(
text=os.path.basename(self.filepaths[0])
)
else:
self.filepath_label.config(
text=f"{len(self.filepaths)}個のファイルが選択されました"
)
if self.trocr_pipeline is not None:
self.analyze_button.config(state=tk.NORMAL)
self.export_button.config(state=tk.DISABLED)
self.status_var.set(f"{len(self.filepaths)}個のファイルを選択")
def start_analysis_thread(self):
if not self.filepaths:
return
self.analyze_button.config(state=tk.DISABLED)
self.select_button.config(state=tk.DISABLED)
self.export_button.config(state=tk.DISABLED)
self.dds_button.config(state=tk.DISABLED)
self.dds_single_button.config(state=tk.DISABLED)
self.all_result = []
self.deal_selector_combo.set("")
self.deal_selector_combo.config(state=tk.DISABLED)
self.status_var.set("分析処理を開始します...")
# 以前の結果をクリア
for player in PLAYER_ORDER:
# self.result_labels[player].config(text="-")
self.player_vars[player] = ""
self.result_entries[player].delete(0, tk.END)
# 分析処理を別スレッドで実行
self.thread = threading.Thread(
target=analyze_image_data,
args=(self.filepaths, self.q, self.trocr_pipeline),
)
self.thread.daemon = True
self.thread.start()
# キューを監視する
self.root.after(100, self.process_queue)
def process_queue(self):
try:
msg = self.q.get_nowait()
if isinstance(msg, list):
# 最終結果が来た場合
self.raw_rank_data = msg
# self.all_result = msg
# self.display_last_results()
self.status_var.set("分析完了!")
self.analyze_button.config(state=tk.NORMAL)
self.select_button.config(state=tk.NORMAL)
self.arrange_data()
self.setup_deal_selector()
if self.raw_rank_data:
self.debugger_button.config(state=tk.NORMAL)
if self.all_result:
self.export_button.config(state=tk.NORMAL)
if any("hands" in result for result in self.all_result):
self.dds_button.config(state=tk.NORMAL)
else:
# 途中の進捗メッセージの場合
self.status_var.set(msg)
self.root.after(
100, self.process_queue
) # 次のメッセージをチェック
except queue.Empty:
# キューが空なら、再度チェック
self.root.after(100, self.process_queue)
def arrange_data(self):
# 生データから最終的な手札を作成・表示
self.all_result = []
temp_hands = {} # ファイルごとの手札を一時保存
for rank_data in self.raw_rank_data:
filename = rank_data["filename"]
if filename not in temp_hands:
temp_hands[filename] = {p: [] for p in PLAYER_ORDER}
color_name = rank_data["color"]
suit = SUITS_BY_COLOR[color_name]
card_name = f"{suit}{rank_data['name']}"
temp_hands[filename][rank_data["player"]].append(card_name)
# 整形してall_resultsに格納
for filename, hands in temp_hands.items():
self.all_result.append(
{
"filename": filename,
"hands": {
player: arrange_hand(cards)
for player, cards in hands.items()
},
}
)
def display_last_results(self):
self.arrange_data()
if not self.all_result:
print("all_result is none")
return
last_result = self.all_result[-1]
if "error" in last_result:
self.filepath_label.config(
text=f"エラー ({last_result['filename']}): {last_result['error']}"
)
elif "hands" in last_result:
self.filepath_label.config(
text=f"最終分析ファイル: {last_result['filename']}"
)
self.last_analyzed_hands = last_result["hands"]
self.dds_button.config(state=tk.NORMAL)
self.dds_single_button.config(state=tk.NORMAL)
for player, hand in last_result["hands"].items():
if player in self.player_vars:
self.result_entries[player].insert(
tk.END, ", ".join(hand) if hand else ""
)
# self.set_text()
# self.player_vars[player].set(
# ", ".join(hand) if hand else ""
# )
# self.result_labels[player].config(
# text=", ".join(hand) if hand else "(なし)"
# )
def setup_deal_selector(self):
successful_files = [
r["filename"] for r in self.all_result if "hands" in r
]
if successful_files:
self.deal_selector_combo["values"] = successful_files
self.deal_selector_combo.config(state="readonly")
self.deal_selector_combo.set(successful_files[0])
self.on_deal_selected()
else:
self.filepath_label.config(
text="分析に成功したディールがありませんでした"
)
def on_deal_selected(self, event=None):
selected_filename = self.deal_selector_combo.get()
if not selected_filename:
return
for result in self.all_result:
if result.get("filename") == selected_filename:
if "hands" in result:
self.filepath_label.config(
text=f"表示中: {result['filename']}"
)
for player, hand in result["hands"].items():
if player in self.player_vars:
self.result_entries[player].delete(0, tk.END)
self.result_entries[player].insert(
tk.END, ", ".join(hand) if hand else ""
)
self.dds_single_button.config(state=tk.NORMAL)
return
def start_dds_analysis(self):
valid_deals = []
for result in self.all_result:
if "hands" in result:
is_valid, _ = validate_deal(result["hands"])
if is_valid:
valid_deals.append(result)
if len(valid_deals) == 0:
messagebox.showwarning(
"分析不可", "分析対象となる正常なディールがありません。"
)
return
self.status_var.set(f"{len(valid_deals)}件のディールを分析中...")
self.root.update_idletasks()
try:
deals = dds.ddTableDealsPBN()
deals.noOfTables = len(valid_deals)
for i, result in enumerate(valid_deals):
pbn_deal_string = convert2pbn_txt(result["hands"], "N")
print(pbn_deal_string)
# table_deal_pbn = dds.ddTableDealPBN()
# table_deal_pbn.cards = pbn_deal_string.encode("utf-8")
deals.deals[i].cards = pbn_deal_string.encode("utf-8")
dds.SetMaxThreads(0)
table_res = dds.ddTablesRes()
per_res = dds.allParResults()
# table_res_pointer = pointer(table_res)
res = dds.CalcAllTablesPBN(
pointer(deals),
0,
(c_int * 5)(0, 0, 0, 0, 0),
pointer(table_res),
pointer(per_res),
)
print("dds")
if res != dds.RETURN_NO_FAULT:
err_char_p = dds.ErrorMessage(res)
err_string = (
string_at(err_char_p).decode("utf-8")
if err_char_p
else "Unknown error"
)
raise RuntimeError(
f"DDS Solver failed with code: {res} ({err_string})"
)
filenames = [d["filename"] for d in valid_deals]
# 3. 結果を新しいウィンドウで表示
DDSResultsWindow(self.root, table_res, filenames, False)
self.status_var.set("ダブルダミー分析が完了しました。")
except Exception as e:
messagebox.showerror(
"DDS分析エラー", f"分析中にエラーが発生しました:\n{e}"
)
self.status_var.set("DDS分析中にエラーが発生しました。")
def start_dds_single_analysis(self):
edited_hands = {
player: parse_hand_string(var.get())
for player, var in self.result_entries.items()
}
self.status_var.set("ディールを分析中...")
self.root.update_idletasks()
try:
print(edited_hands)
deals = dds.ddTableDealPBN()
pbn_deal_string = convert2pbn_txt(edited_hands, "N")
print(pbn_deal_string)
# table_deal_pbn = dds.ddTableDealPBN()
# table_deal_pbn.cards = pbn_deal_string.encode("utf-8")
deals.cards = pbn_deal_string.encode("utf-8")
table_res = dds.ddTableResults()
# table_res_pointer = pointer(table_res)
res = dds.CalcDDtablePBN(deals, table_res)
print("dds")
if res != dds.RETURN_NO_FAULT:
err_char_p = dds.ErrorMessage(res)
err_string = (
string_at(err_char_p).decode("utf-8")
if err_char_p
else "Unknown error"
)
raise RuntimeError(
f"DDS Solver failed with code: {res} ({err_string})"
)
# 3. 結果を新しいウィンドウで表示
DDSResultsWindow(self.root, table_res, ["現在のハンド"], True)
self.status_var.set("ダブルダミー分析が完了しました。")
except Exception as e:
messagebox.showerror(
"DDS分析エラー", f"分析中にエラーが発生しました:\n{e}"
)
self.status_var.set("DDS分析中にエラーが発生しました。")
def open_debugger(self):
if not self.raw_rank_data:
messagebox.showwarning(
"データなし",
"デバッグするデータがありません。まず画像を分析してください。",
)
return
ColorDebuggerWindow(self.root, self.raw_rank_data)
def export_results(self):
if not self.all_result:
messagebox.showwarning(
"エクスポート不可", "エクスポートするデータがありません。"
)
return
save_path = filedialog.asksaveasfilename(
title="結果を保存",
defaultextension=".xhd",
filetypes=[
("xhdファイル", "*.xhd"),
("dupファイル", "*.dup"),
("pbnファイル", "*.pbn"),
],
initialfile=f"card_analysis_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
)
if not save_path:
return
filename, ext = os.path.splitext(os.path.basename(save_path))
encoding = "utf-8"
if ext == ".xhd":
text = convert2xhd(self.all_result, filename)
encoding = "shift_jis"
elif ext == ".dup":
text = convert2dup(self.all_result, filename)
elif ext == ".pbn":
text = convert2pbn(self.all_result, filename)
else:
text = convert2txt(self.all_result, filename)
print(text)
try:
with open(save_path, "w", encoding=encoding) as f:
f.write(text)
messagebox.showinfo(
"成功", f"結果が正常に保存されました:\n{save_path}"
)
self.status_var.set("結果をテキストファイルに出力しました。")
except Exception as e:
messagebox.showerror(
"エラー", f"ファイルへの書き込み中にエラーが発生しました:\n{e}"
)
class ColorDebuggerWindow(tk.Toplevel):
def __init__(self, parent, raw_data):
super().__init__(parent)
self.title("カラーデバッガー")
self.geometry("800x700")
self.raw_data = raw_data
# Entryウィジェット用の変数を初期化
self.l_thresh_var = tk.StringVar(
value=str(DEFAULT_THRESHOLDS["L_black"])
)
self.a_green_thresh_var = tk.StringVar(
value=str(DEFAULT_THRESHOLDS["a_green"])
)
self.a_black_thresh_var = tk.StringVar(
value=str(DEFAULT_THRESHOLDS["a_black"])
)
self.b_black_thresh_var = tk.StringVar(
value=str(DEFAULT_THRESHOLDS["b_black"])
)
self.a_red_thresh_var = tk.StringVar(
value=str(DEFAULT_THRESHOLDS["a_red"])
)
self.b_orange_thresh_var = tk.StringVar(
value=str(DEFAULT_THRESHOLDS["b_orange"])
)
# --- GUIレイアウト ---
# 制御フレーム
control_frame = ttk.Frame(self, padding=10)
control_frame.pack(fill=tk.X)
# 数値入力ボックス (Entry) を作成
ttk.Label(control_frame, text="L(黒)<").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(control_frame, textvariable=self.l_thresh_var, width=8).pack(
side=tk.LEFT, padx=(0, 10)
)
ttk.Label(control_frame, text="a(緑)<").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(
control_frame, textvariable=self.a_green_thresh_var, width=8
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(control_frame, text="a(黒)>").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(
control_frame, textvariable=self.a_black_thresh_var, width=8
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(control_frame, text="b(黒)<").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(
control_frame, textvariable=self.b_black_thresh_var, width=8
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(control_frame, text="a(赤)>").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(
control_frame, textvariable=self.a_red_thresh_var, width=8
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(control_frame, text="b(橙)>").pack(side=tk.LEFT, padx=(0, 2))
ttk.Entry(
control_frame, textvariable=self.b_orange_thresh_var, width=8
).pack(side=tk.LEFT, padx=(0, 10))
# 更新ボタン
ttk.Button(
control_frame,
text="閾値を更新して再判定",
command=self.update_classifications,
).pack(side=tk.LEFT, padx=20)
# 結果表示用のキャンバスとスクロールバー
canvas_frame = ttk.Frame(self)
canvas_frame.pack(fill=tk.BOTH, expand=True)
self.canvas = tk.Canvas(canvas_frame)
scrollbar = ttk.Scrollbar(
canvas_frame, orient="vertical", command=self.canvas.yview
)
self.scrollable_frame = ttk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")
),
)
self.canvas.create_window(
(0, 0), window=self.scrollable_frame, anchor="nw"
)
self.canvas.configure(yscrollcommand=scrollbar.set)
self.canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
self.photo_images = []
self.update_classifications()
def update_classifications(self, event=None):
# 既存のウィジェットをクリア
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
try:
# 入力された文字列を数値に変換
thresholds = DEFAULT_THRESHOLDS.copy()
thresholds["L_black"] = float(self.l_thresh_var.get())
thresholds["a_green"] = float(self.a_green_thresh_var.get())
thresholds["a_red"] = float(self.a_red_thresh_var.get())
thresholds["a_black"] = float(self.a_black_thresh_var.get())
thresholds["b_black"] = float(self.b_black_thresh_var.get())
thresholds["b_orange"] = float(self.b_orange_thresh_var.get())
except ValueError:
messagebox.showerror(
"入力エラー", "閾値には数値を入力してください。"
)
return
# 既存のウィジェットをクリア
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
self.photo_images.clear() # 参照をクリア
for i, rank_data in enumerate(self.raw_data):
avg_lab = rank_data["avg_lab"]
color_name, avg_lab = get_suit_from_color_rules(
avg_lab, thresholds
)
rank_data["color"] = color_name
# --- 各ランクの情報を表示 ---
row_frame = ttk.Frame(self.scrollable_frame, padding=5)
row_frame.pack(fill=tk.X)
# 画像パッチを表示
pil_img = Image.fromarray(
cv2.cvtColor(rank_data["img"], cv2.COLOR_BGR2RGB)
)
pil_img.thumbnail((40, 50))
photo_img = ImageTk.PhotoImage(pil_img)
self.photo_images.append(photo_img)
ttk.Label(row_frame, image=photo_img).pack(side=tk.LEFT)
# 情報をテキストで表示
info_text = (
f"Player: {rank_data["player"]} | Rank: {rank_data['name']} | "
f"L:{avg_lab[0]:.1f} a:{avg_lab[1]:.1f} b:{avg_lab[2]:.1f} -> "
f"判定: {color_name.upper()}"
)
ttk.Label(
row_frame, text=info_text, font=("Courier New", 10)
).pack(side=tk.LEFT, padx=10)
class DDSResultsWindow(tk.Toplevel):
def __init__(self, parent, solved_table, filenames, is_single):
super().__init__(parent)
self.title("ダブルダミー分析結果")
self.geometry("490x180")
notebook = ttk.Notebook(self)
notebook.pack(pady=10, padx=10, fill="both", expand=True)
# print(solved_table.noOfBoards)
if is_single:
frame = ttk.Frame(self, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# ★★★ 重要な修正点:columnsタプルに 'player' を明確に含める ★★★
column_ids = ("player", "nt", "s", "h", "d", "c")
tree = ttk.Treeview(frame, columns=column_ids, show="headings")
tree.pack(fill=tk.BOTH, expand=True)
# ヘッダー(列のタイトル)の設定
tree.heading("player", text="Declarer")
tree.heading("nt", text="NT")
tree.heading("s", text="Spades ♠")
tree.heading("h", text="Hearts ♥")
tree.heading("d", text="Diamonds ♦")
tree.heading("c", text="Clubs ♣")
# 各列の幅と文字揃えを設定
tree.column("player", width=80, anchor=tk.W, stretch=tk.NO)
tree.column("nt", width=50, anchor=tk.CENTER)
tree.column("s", width=80, anchor=tk.CENTER)
tree.column("h", width=80, anchor=tk.CENTER)
tree.column("d", width=80, anchor=tk.CENTER)
tree.column("c", width=80, anchor=tk.CENTER)
# DDSライブラリの規約に合わせて、表示するプレイヤーの順番を定義
players_map = {0: "North", 1: "East", 2: "South", 3: "West"}
suits_map = {4: "nt", 0: "s", 1: "h", 2: "d", 3: "c"}
# テーブルデータを整形
table_data = {p_name: {} for p_name in players_map.values()}
for suit_idx, suit_name in suits_map.items():
for player_idx, player_name in players_map.items():
tricks = solved_table.resTable[suit_idx][player_idx]
table_data[player_name][suit_name] = tricks
# テーブルにデータを挿入
for player_name in ["North", "South", "East", "West"]:
# valuesの最初の要素が 'player' カラムに対応
row_values = [player_name] + [
table_data[player_name][s]
for s in ["nt", "s", "h", "d", "c"]
]
tree.insert("", tk.END, values=tuple(row_values))
else:
for i in range(solved_table.noOfBoards // 20):
filename = filenames[i]
table = solved_table.results[i]
frame = ttk.Frame(notebook, padding="10")
notebook.add(frame, text=os.path.basename(filename)[:20])
# Treeviewウィジェットでテーブルを作成
tree = ttk.Treeview(
frame,
columns=("player", "nt", "s", "h", "d", "c"),
show="headings",
height=5,
)
tree.pack(fill=tk.BOTH, expand=True)
# ヘッダーの設定
tree.heading("player", text="Declarer")
tree.heading("nt", text="NT")
tree.heading("s", text="Spades ♠")
tree.heading("h", text="Hearts ♥")
tree.heading("d", text="Diamonds ♦")
tree.heading("c", text="Clubs ♣")
tree.column("player", width=80, anchor=tk.W, stretch=tk.NO)
tree.column("nt", width=60, anchor=tk.CENTER)
tree.column("s", width=80, anchor=tk.CENTER)
tree.column("h", width=80, anchor=tk.CENTER)
tree.column("d", width=80, anchor=tk.CENTER)
tree.column("c", width=80, anchor=tk.CENTER)
table_data = reshape_table(table)
print(table_data)
# テーブルにデータを挿入
for player_name in ["North", "South", "East", "West"]:
row_values = [player_name] + [
table_data[player_name][s]
for s in ["nt", "s", "h", "d", "c"]
]
tree.insert("", tk.END, values=tuple(row_values))
if __name__ == "__main__":
root = tk.Tk()
app = CardRecognizerApp(root)
root.mainloop()