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( "<>", 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( "", 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()