wai572 commited on
Commit
27db1bc
·
1 Parent(s): cdfc8fa
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ libdds.so filter=lfs diff=lfs merge=lfs -text
37
+ libdds.a filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12.1
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
7
+
8
+ COPY ./. /code/
9
+
10
+ # uvicornを起動
11
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ import tempfile
4
+ from ctypes import c_int, c_uint, pointer, string_at
5
+ from datetime import datetime
6
+
7
+ import cv2
8
+ import gradio as gr
9
+ import numpy as np
10
+ import pandas as pd
11
+ from gradio_modal import Modal
12
+ from PIL import Image
13
+ from transformers import pipeline
14
+
15
+ import dds
16
+ from identify_cards import (
17
+ SUIT_TEMPLATE_PATH,
18
+ determine_and_correct_orientation,
19
+ find_rank_candidates,
20
+ get_suit_from_image_rules,
21
+ load_suit_templates,
22
+ save_img_with_rect,
23
+ )
24
+ from utils import (
25
+ arrange_hand,
26
+ convert2dup,
27
+ convert2pbn,
28
+ convert2pbn_board,
29
+ convert2pbn_txt,
30
+ convert2xhd,
31
+ is_text_valid,
32
+ )
33
+
34
+ # --- グローバル変数・設定 ---
35
+ trocr_pipeline = None
36
+
37
+ SUITS_BY_COLOR = {"black": "S", "green": "C", "red": "H", "orange": "D"}
38
+ VALID_RANKS = ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"]
39
+ PLAYER_ORDER = ["north", "east", "south", "west"]
40
+ SUIT_ORDER = {"S": 0, "H": 1, "D": 2, "C": 3}
41
+ RANK_ORDER = {
42
+ "A": 14,
43
+ "K": 13,
44
+ "Q": 12,
45
+ "J": 11,
46
+ "T": 10,
47
+ "9": 9,
48
+ "8": 8,
49
+ "7": 7,
50
+ "6": 6,
51
+ "5": 5,
52
+ "4": 4,
53
+ "3": 3,
54
+ "2": 2,
55
+ }
56
+ DEFAULT_THRESHOLDS = {
57
+ "L_black": 65.0,
58
+ "a_green": 126.0,
59
+ "a_red": 134.0,
60
+ "ba_black": -4.5,
61
+ "ab_black": 250.0,
62
+ "a_b_red": 9.0,
63
+ }
64
+
65
+
66
+ def load_model():
67
+ """
68
+ TrOCRモデルをバックグラウンドで読み込む関数。
69
+ UIのロード完了後に demo.load() イベントで呼び出される。
70
+ """
71
+ global trocr_pipeline
72
+ try:
73
+ if trocr_pipeline is None:
74
+ print("バックグラウンドでTrOCRモデルを読み込んでいます...")
75
+ trocr_pipeline = pipeline(
76
+ "image-to-text", model="microsoft/trocr-small-printed"
77
+ )
78
+ print("TrOCRの準備が完了しました。")
79
+
80
+ # UIコンポーネントを更新するための値を返す
81
+ return gr.update(
82
+ value="準備完了。画像を選択して分析を開始してください。"
83
+ ), gr.update(interactive=True)
84
+
85
+ except Exception as e:
86
+ error_message = f"AIモデルの読み込みエラー: {e}"
87
+ print(error_message)
88
+ gr.Warning(
89
+ f"AIモデルの読み込みに失敗しました。分析機能は利用できません。詳細はログを確認してください。"
90
+ )
91
+ # エラーメッセージを表示し、分析ボタンは無効のままにする
92
+ return gr.update(value=error_message), gr.update(interactive=False)
93
+
94
+
95
+ def get_player_regions(img, box, margin):
96
+ bx, by, bw, bh = box
97
+ h, w, _ = img.shape
98
+ player_regions = {
99
+ "north": img[0:by, :],
100
+ "south": img[by + bh : h, :],
101
+ "west": img[by - margin : by + bh + margin, 0:bx],
102
+ "east": img[by - margin : by + bh + margin, bx + bw : w],
103
+ }
104
+
105
+ for player, region in player_regions.items():
106
+ if region is not None and region.size > 0:
107
+ if player == "north":
108
+ player_regions[player] = cv2.rotate(region, cv2.ROTATE_180)
109
+ elif player == "east":
110
+ player_regions[player] = cv2.rotate(
111
+ region, cv2.ROTATE_90_CLOCKWISE
112
+ )
113
+ elif player == "west":
114
+ player_regions[player] = cv2.rotate(
115
+ region, cv2.ROTATE_90_COUNTERCLOCKWISE
116
+ )
117
+
118
+ return player_regions
119
+
120
+
121
+ def arrange_data(raw_rank_data):
122
+ # 生データから最終的な手札を作成・表示
123
+ all_result = []
124
+ temp_hands = {} # ファイルごとの手札を一時保存
125
+
126
+ for rank_data in raw_rank_data:
127
+ filename = rank_data["filename"]
128
+ if filename not in temp_hands:
129
+ temp_hands[filename] = {p: [] for p in PLAYER_ORDER}
130
+
131
+ color_name = rank_data["color"]
132
+ suit = SUITS_BY_COLOR[color_name]
133
+ card_name = f"{suit}{rank_data['name']}"
134
+ temp_hands[filename][rank_data["player"]].append(card_name)
135
+
136
+ # 整形してall_resultsに格納
137
+ for filename, hands in temp_hands.items():
138
+ all_result.append(
139
+ {
140
+ "filename": filename,
141
+ "hands": {
142
+ player: arrange_hand(cards)
143
+ for player, cards in hands.items()
144
+ },
145
+ }
146
+ )
147
+ return all_result
148
+
149
+
150
+ def analyze_image_gradio(image_paths, progress=gr.Progress()):
151
+ global trocr_pipeline
152
+ # モデルが読み込まれているか確認
153
+ if trocr_pipeline is None:
154
+ gr.Warning(
155
+ "AIモデルがまだ読み込まれていません。しばらく待ってから再度お試しください。"
156
+ )
157
+ # 空の更新を返すことで、UIの状態を変えずに処理を終了
158
+ return (gr.update(),) * 11
159
+
160
+ all_results = []
161
+ num_total_files = len(image_paths)
162
+
163
+ progress(0, desc="テンプレート画像読み込み中...")
164
+ suit_templates = load_suit_templates(SUIT_TEMPLATE_PATH)
165
+ if not suit_templates:
166
+ raise gr.Error(
167
+ f"エラー: {SUIT_TEMPLATE_PATH} フォルダにスートのテンプレート画像が見つかりません。"
168
+ )
169
+
170
+ try:
171
+ all_candidates_global = []
172
+ processed_files_info = []
173
+ # image_objects = {}
174
+
175
+ for i, image_path in enumerate(image_paths):
176
+ progress(
177
+ (i + 1) / num_total_files * 0.15,
178
+ desc="ステージ1/3: 文字候補を検出中...",
179
+ )
180
+ filename = os.path.basename(image_path)
181
+ progress(
182
+ (i + 1) / num_total_files * 0.3,
183
+ f"分析中 ({i+1}/{num_total_files}): {filename}",
184
+ )
185
+
186
+ try:
187
+ # ファイルをバイナリモードで安全に読み込む
188
+ with open(image_path, "rb") as f:
189
+ # バイトデータをNumPy配列に変換
190
+ file_bytes = np.asarray(
191
+ bytearray(f.read()), dtype=np.uint8
192
+ )
193
+ # NumPy配列(メモリ上のデータ)から画像をデコード
194
+ image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
195
+
196
+ if image is None:
197
+ raise gr.Error(
198
+ "OpenCVが画像をデコードできませんでした。ファイルが破損しているか、非対応の形式の可能性があります。"
199
+ )
200
+ # image_objects[filename] = image
201
+ except Exception as e:
202
+ # ファイル読み込み自体のエラーをキャッチ
203
+ all_results.append(
204
+ {"filename": filename, "error": f"画像読み込みエラー: {e}"}
205
+ )
206
+ # image_objects[filename] = None
207
+ continue
208
+
209
+ # box = find_center_box(image)
210
+ print("detect board")
211
+ rotated_image, box, scale = determine_and_correct_orientation(
212
+ image, lambda msg: print(msg)
213
+ )
214
+ if box is None:
215
+ all_results.append(
216
+ {"filename": filename, "error": "中央ボードの検出に失敗"}
217
+ )
218
+ continue
219
+ print(box)
220
+ save_img_with_rect("debug_rotated.jpg", rotated_image, [box])
221
+
222
+ MARGIN = 200
223
+ player_regions = get_player_regions(rotated_image, box, MARGIN)
224
+
225
+ for player, region in player_regions.items():
226
+ candidates = find_rank_candidates(
227
+ region, suit_templates, player, scale
228
+ )
229
+ for cand in candidates:
230
+ cand["filename"] = filename
231
+ cand["player"] = player
232
+ all_candidates_global.append(cand)
233
+
234
+ processed_files_info.append({"filename": filename, "error": None})
235
+ progress(
236
+ 0.4, desc="ステージ2/3: 文字認識を実行中... (時間がかかります)"
237
+ )
238
+
239
+ if not all_candidates_global or not trocr_pipeline:
240
+ progress(1, desc="認識する文字候補がありませんでした。")
241
+ print("認識する文字候補がありませんでした。")
242
+ return all_results # エラーがあった画像の結果だけを返す
243
+
244
+ try:
245
+ candidates_pil_images = [
246
+ Image.fromarray(cv2.cvtColor(c["img"], cv2.COLOR_BGR2RGB))
247
+ for c in all_candidates_global
248
+ ]
249
+ ocr_results = trocr_pipeline(candidates_pil_images)
250
+ except Exception as e:
251
+ gr.Warning(f"OCR処理中にエラーが発生しました: {e}")
252
+
253
+ # --- ステージ3: 結果の仕分けと最終的なカードの特定 ---
254
+ progress(0.9, desc="ステージ3/3: 認識結果を仕分け中...")
255
+
256
+ print([result[0]["generated_text"] for result in ocr_results])
257
+
258
+ raw_data = []
259
+ # blacks = []
260
+ # reds = []
261
+ for i, result in enumerate(ocr_results):
262
+ text = result[0]["generated_text"].upper().strip()
263
+ print(text, is_text_valid(text))
264
+
265
+ text = is_text_valid(text)
266
+ if text is not None:
267
+ candidate_info = all_candidates_global[i]
268
+ print(
269
+ f"--- 診断中: ランク '{text}' of {candidate_info['player']} at {candidate_info['pos']} with thick:{candidate_info['thickness']} ---"
270
+ )
271
+ color_name, avg_lab = get_suit_from_image_rules(
272
+ candidate_info["no_pad"], DEFAULT_THRESHOLDS
273
+ )
274
+ print(color_name)
275
+ if color_name == "mark":
276
+ continue
277
+ candidate_info["avg_lab"] = avg_lab
278
+ candidate_info["color"] = color_name
279
+ candidate_info["name"] = text
280
+ raw_data.append(candidate_info)
281
+
282
+ # print("\r\n".join(blacks))
283
+ # print("\r\n".join(reds))
284
+
285
+ all_results = arrange_data(raw_data)
286
+ pbn_content = convert2pbn(all_results)
287
+ pbn_filename = f"analysis_{datetime.now().strftime('%Y%m%d')}.pbn"
288
+ # if processed_files_info:
289
+ # last_result = {"filename": processed_files_info[0]["filename"], 1ands": all_results[0][1ands"]}
290
+
291
+ if all_results:
292
+ # ダウンロード用にPBNコンテンツを値として設定し、表示状態にする
293
+ export_update = gr.update(interactive=True)
294
+ else:
295
+ export_update = gr.update(interactive=False)
296
+ final_result = all_results[0]["hands"]
297
+ filenames = [os.path.basename(p) for p in image_paths]
298
+ dropdown_update = gr.update(
299
+ choices=filenames, value=filenames[0], interactive=True, open=True
300
+ )
301
+
302
+ dataframes = run_dds_analysis(all_results, progress)
303
+ for result in all_results:
304
+ if result["filename"] in dataframes.keys():
305
+ result["dds"] = dataframes[result["filename"]]
306
+
307
+ return (
308
+ *display_selected_result(filenames[0], all_results),
309
+ all_results,
310
+ dropdown_update,
311
+ export_update,
312
+ )
313
+
314
+ except Exception as e:
315
+ raise gr.Error(f"致命的なエラー: {e}")
316
+
317
+
318
+ def display_selected_result(selected_filename, all_results):
319
+ """ドロップダウンで選択されたファイルの結果を表示する"""
320
+ result = next(
321
+ (r for r in all_results if r["filename"] == selected_filename), None
322
+ )
323
+
324
+ output_hands = {p: "" for p in PLAYER_ORDER}
325
+ dds_df_update = gr.update(value=None)
326
+
327
+ if result and "hands" in result and result["hands"]:
328
+ for player, hand in result["hands"].items():
329
+ output_hands[player] = ", ".join(hand) if hand else "(なし)"
330
+ dds_visible = True
331
+ dds_df_update = gr.update(value=result.get("dds"), visible=True)
332
+
333
+ elif result and "error" in result:
334
+ # エラーがあった場合、最初のTextboxにエラーメッセージを表示
335
+ output_hands["north"] = f"エラー: {result['error']}"
336
+
337
+ return (
338
+ output_hands["north"],
339
+ output_hands["south"],
340
+ output_hands["west"],
341
+ output_hands["east"],
342
+ dds_df_update,
343
+ )
344
+
345
+
346
+ def validate_deal(hands):
347
+ if not hands:
348
+ return False, "分析対象のカードデータがありません"
349
+ total_cards = []
350
+
351
+ # 手札が合計52枚あるかチェック
352
+ for player, hand in hands.items():
353
+ if len(hand) != 13:
354
+ return (
355
+ False,
356
+ f"エラー: {player.capitalize()}の手札が13枚ではありません",
357
+ )
358
+ for card in hand:
359
+ if card in total_cards:
360
+ return (
361
+ False,
362
+ f"エラー: 重複したカードが検出されました ({card})",
363
+ )
364
+ total_cards.append(card)
365
+
366
+ return True, "デックは正常です"
367
+
368
+
369
+ def format_dds_data(table):
370
+ headers = [
371
+ "Declarer",
372
+ "NT",
373
+ "Spades ♠",
374
+ "Hearts ♥",
375
+ "Diamonds ♦",
376
+ "Clubs ♣",
377
+ ]
378
+ rows = [
379
+ [
380
+ "North",
381
+ table[4][0],
382
+ table[0][0],
383
+ table[1][0],
384
+ table[2][0],
385
+ table[3][0],
386
+ ],
387
+ [
388
+ "South",
389
+ table[4][2],
390
+ table[0][2],
391
+ table[1][2],
392
+ table[2][2],
393
+ table[3][2],
394
+ ],
395
+ [
396
+ "East",
397
+ table[4][1],
398
+ table[0][1],
399
+ table[1][1],
400
+ table[2][1],
401
+ table[3][1],
402
+ ],
403
+ [
404
+ "West",
405
+ table[4][3],
406
+ table[0][3],
407
+ table[1][3],
408
+ table[2][3],
409
+ table[3][3],
410
+ ],
411
+ ]
412
+ return headers, rows
413
+
414
+
415
+ def run_dds_analysis(all_results_state, progress=gr.Progress()):
416
+ """ダブルダミー分析を実行する"""
417
+ valid_deals = []
418
+ for result in all_results_state:
419
+ if "hands" in result:
420
+ is_valid, _ = validate_deal(result["hands"])
421
+ if is_valid:
422
+ valid_deals.append(result)
423
+
424
+ if len(valid_deals) == 0:
425
+ raise gr.Error(
426
+ "分析不可", "分析対象となる正常なディールがありません。"
427
+ )
428
+
429
+ # self.status_var.set(f"{len(valid_deals)}件のディールを分析中...")
430
+
431
+ try:
432
+ deals = dds.ddTableDealsPBN()
433
+ deals.noOfTables = len(valid_deals)
434
+ for i, result in enumerate(valid_deals):
435
+ pbn_deal_string = convert2pbn_txt(result["hands"], "N")
436
+ print(pbn_deal_string)
437
+
438
+ # table_deal_pbn = dds.ddTableDealPBN()
439
+ # table_deal_pbn.cards = pbn_deal_string.encode("utf-8")
440
+
441
+ deals.deals[i].cards = pbn_deal_string.encode("utf-8")
442
+
443
+ dds.SetMaxThreads(0)
444
+ table_res = dds.ddTablesRes()
445
+ per_res = dds.allParResults()
446
+ # table_res_pointer = pointer(table_res)
447
+ res = dds.CalcAllTablesPBN(
448
+ pointer(deals),
449
+ 0,
450
+ (c_int * 5)(0, 0, 0, 0, 0),
451
+ pointer(table_res),
452
+ pointer(per_res),
453
+ )
454
+ print("dds")
455
+
456
+ if res != dds.RETURN_NO_FAULT:
457
+ err_char_p = dds.ErrorMessage(res)
458
+ err_string = (
459
+ string_at(err_char_p).decode("utf-8")
460
+ if err_char_p
461
+ else "Unknown error"
462
+ )
463
+ raise RuntimeError(
464
+ f"DDS Solver failed with code: {res} ({err_string})"
465
+ )
466
+ print("dds")
467
+
468
+ filenames = [d["filename"] for d in valid_deals]
469
+ dataframes = {}
470
+ for i, filename in enumerate(filenames):
471
+ headers, rows = format_dds_data(table_res.results[i].resTable)
472
+ print(rows)
473
+ dataframes[filename] = pd.DataFrame(rows, columns=headers)
474
+
475
+ return dataframes
476
+
477
+ # 3. 結果を新しいウィンドウで表示
478
+
479
+ except Exception as e:
480
+ raise gr.Error(f"DDS分析エラー: 分析中にエラーが発生しました:\n{e}")
481
+ # self.status_var.set("DDS分析中にエラーが発生しました。")
482
+
483
+
484
+ def prepare_export_files(all_results):
485
+ """エクスポートボタンが押されたときに各形式のファイルを生成し、ダウンロードボタンを返す"""
486
+ if not all_results:
487
+ gr.Warning("エクスポート対象のデータがありません。")
488
+ return (
489
+ gr.update(visible=False),
490
+ gr.update(visible=False),
491
+ gr.update(visible=False),
492
+ gr.update(visible=True),
493
+ )
494
+
495
+ filename_base, _ = os.path.splitext(all_results[0]["filename"])
496
+
497
+ # --- PBN ---
498
+ pbn_content = convert2pbn(all_results)
499
+ with tempfile.NamedTemporaryFile(
500
+ delete=False, mode="w", suffix=".pbn", encoding="utf-8"
501
+ ) as f:
502
+ f.write(pbn_content)
503
+ pbn_path = f.name
504
+
505
+ # --- XHD ---
506
+ xhd_content = convert2xhd(all_results, filename_base)
507
+ with tempfile.NamedTemporaryFile(
508
+ delete=False,
509
+ mode="w",
510
+ suffix=".xhd",
511
+ encoding="shift_jis",
512
+ errors="ignore",
513
+ ) as f:
514
+ f.write(xhd_content)
515
+ xhd_path = f.name
516
+
517
+ # --- DUP ---
518
+ dup_content = convert2dup(all_results, None)
519
+ with tempfile.NamedTemporaryFile(
520
+ delete=False, mode="w", suffix=".dup", encoding="utf-8"
521
+ ) as f:
522
+ f.write(dup_content)
523
+ dup_path = f.name
524
+
525
+ return (
526
+ gr.update(value=pbn_path, visible=True),
527
+ gr.update(value=xhd_path, visible=True),
528
+ gr.update(value=dup_path, visible=True),
529
+ gr.update(visible=True), # モーダルを表示
530
+ )
531
+
532
+
533
+ # --- Gradio UIの定義 ---
534
+ with gr.Blocks(
535
+ theme=gr.themes.Soft(), css="footer {visibility: hidden}"
536
+ ) as demo:
537
+ # 状態を保持するための非表示コンポーネント
538
+ all_results_state = gr.State([])
539
+ raw_data_state = gr.State([])
540
+ current_result_state = gr.State(None)
541
+
542
+ gr.Markdown("# Bridge Card Recognizer")
543
+ gr.Markdown(
544
+ "カメラで撮影したブリッジのプレイ中の写真から、各プレイヤーの手札を自動で認識します。"
545
+ )
546
+
547
+ with gr.Row():
548
+ with gr.Column(scale=2):
549
+ image_input = gr.File(
550
+ label="画像ファイルを選択",
551
+ file_count="multiple",
552
+ file_types=["image"],
553
+ type="filepath",
554
+ )
555
+ analyze_button = gr.Button(
556
+ "分析開始", variant="primary", interactive=False
557
+ )
558
+ export_button = gr.Button(
559
+ "結果をエクスポート", interactive=False
560
+ ) # 新しいエクスポートボタン
561
+ status_label = gr.Label(
562
+ label="ステータス",
563
+ value="準備完了。画像を選択して分析を開始してください。",
564
+ )
565
+
566
+ if not trocr_pipeline:
567
+ gr.Warning(
568
+ "OCRモデルの読み込みに失敗しました。分析機能は利用できません。"
569
+ )
570
+
571
+ with gr.Column(scale=3):
572
+ results_dropdown = gr.Dropdown(
573
+ label="表示するファイルを選択", interactive=False
574
+ )
575
+ gr.Markdown("### 認識結果")
576
+ with gr.Row():
577
+ north_box = gr.Textbox(label="North", interactive=False)
578
+ south_box = gr.Textbox(label="South", interactive=False)
579
+ with gr.Row():
580
+ west_box = gr.Textbox(label="West", interactive=False)
581
+ east_box = gr.Textbox(label="East", interactive=False)
582
+
583
+ with gr.Row():
584
+ # dds_button = gr.Button("ダブルダミー分析 (DDS)", visible=False)
585
+ # debugger_button = gr.Button("カラーデバッガー", visible=False)
586
+ export_file = gr.File(
587
+ label="ダウンロード", visible=False, interactive=False
588
+ )
589
+
590
+ with gr.Accordion("ダブルダミー分析 結果", open=False):
591
+ dds_output_df = gr.DataFrame(
592
+ label="最適プレイ手数", visible=False
593
+ )
594
+ # エクスポート用モーダル
595
+ with Modal(visible=False) as export_modal:
596
+ gr.Markdown("### エクスポート形式を選択してください")
597
+ gr.Markdown(
598
+ "ボタンをクリックすると、対応する形式のファイルがダウンロードされます。"
599
+ )
600
+ with gr.Row():
601
+ pbn_dl_btn = gr.DownloadButton("PBN形式 (.pbn)", variant="primary")
602
+ xhd_dl_btn = gr.DownloadButton(
603
+ "XHD形式 (.xhd)", variant="secondary"
604
+ )
605
+ dup_dl_btn = gr.DownloadButton(
606
+ "DUP形式 (.dup)", variant="secondary"
607
+ )
608
+ # --- イベントリスナー ---
609
+ demo.load(
610
+ fn=load_model, inputs=None, outputs=[status_label, analyze_button]
611
+ )
612
+
613
+ analyze_button.click(
614
+ fn=analyze_image_gradio,
615
+ inputs=[image_input],
616
+ outputs=[
617
+ north_box,
618
+ south_box,
619
+ west_box,
620
+ east_box,
621
+ dds_output_df,
622
+ all_results_state,
623
+ results_dropdown,
624
+ export_button,
625
+ ],
626
+ )
627
+
628
+ results_dropdown.change(
629
+ fn=display_selected_result,
630
+ inputs=[results_dropdown, all_results_state],
631
+ outputs=[north_box, south_box, west_box, east_box, dds_output_df],
632
+ )
633
+ export_button.click(
634
+ fn=prepare_export_files,
635
+ inputs=[all_results_state],
636
+ outputs=[pbn_dl_btn, xhd_dl_btn, dup_dl_btn, export_modal],
637
+ )
638
+
639
+ # dds_button.click(
640
+ # fn=run_dds_analysis,
641
+ # inputs=[all_results_state],
642
+ # outputs=[dds_output_df],
643
+ # )
644
+
645
+
646
+ if __name__ == "__main__":
647
+ demo.launch(debug=True)
dds.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #! /usr/bin/python
2
+
3
+ """Copyright 2014 - 2015 Foppe HEMMINGA
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License."""
16
+
17
+ import os
18
+ from ctypes import *
19
+
20
+ script_path = os.path.abspath(__file__)
21
+ # このファイルがあるディレクトリ(つまり 'dds' フォルダ)のパスを取得
22
+ script_dir = os.path.dirname(script_path)
23
+ # 1つ上の階層(プロジェクトルート)にあるはずのdllへのパスを構築
24
+ # これにより、実行場所に関わらず、常に正しい場所のdllを探しに行きます。
25
+ dll_path = os.path.join(script_dir, "libdds.so")
26
+ # パスを正規化(例: C:\path\dds\..\dds.dll -> C:\path\dds.dll)
27
+ dll_path = os.path.normpath(dll_path)
28
+ dds = cdll.LoadLibrary(dll_path)
29
+ print("Loaded lib {0}".format(dds))
30
+
31
+ DDS_VERSION = 20700
32
+
33
+ DDS_HANDS = 4
34
+ DDS_SUITS = 4
35
+ DDS_STRAINS = 5
36
+
37
+ MAXNOOFBOARDS = 200
38
+
39
+ RETURN_NO_FAULT = 1
40
+
41
+
42
+ class futureTricks(Structure):
43
+ _fields_ = [
44
+ ("nodes", c_int),
45
+ ("cards", c_int),
46
+ ("suit", c_int * 13),
47
+ ("rank", c_int * 13),
48
+ ("equals", c_int * 13),
49
+ ("score", c_int * 13),
50
+ ]
51
+
52
+
53
+ class deal(Structure):
54
+ _fields_ = [
55
+ ("trump", c_int),
56
+ ("first", c_int),
57
+ ("currentTrickSuit", c_int * 3),
58
+ ("currentTrickRank", c_int * 3),
59
+ ("remainCards", c_int * DDS_HANDS * DDS_SUITS),
60
+ ]
61
+
62
+
63
+ class dealPBN(Structure):
64
+ _fields_ = [
65
+ ("trump", c_int),
66
+ ("first", c_int),
67
+ ("currentTrickSuit", c_int * 3),
68
+ ("currentTrickRank", c_int * 3),
69
+ ("remainCards", c_char * 80),
70
+ ]
71
+
72
+
73
+ class boards(Structure):
74
+ _fields_ = [
75
+ ("noOfBoards", c_int),
76
+ ("deals", deal * MAXNOOFBOARDS),
77
+ ("target", c_int * MAXNOOFBOARDS),
78
+ ("solutions", c_int * MAXNOOFBOARDS),
79
+ ("mode", c_int * MAXNOOFBOARDS),
80
+ ]
81
+
82
+
83
+ class boardsPBN(Structure):
84
+ _fields_ = [
85
+ ("noOfBoards", c_int),
86
+ ("deals", dealPBN * MAXNOOFBOARDS),
87
+ ("target", c_int * MAXNOOFBOARDS),
88
+ ("solutions", c_int * MAXNOOFBOARDS),
89
+ ("mode", c_int * MAXNOOFBOARDS),
90
+ ]
91
+
92
+
93
+ class solvedBoards(Structure):
94
+ _fields_ = [
95
+ ("noOfBoards", c_int),
96
+ ("solvedBoards", futureTricks * MAXNOOFBOARDS),
97
+ ]
98
+
99
+
100
+ class ddTableDeal(Structure):
101
+ _fields_ = [("cards", c_uint * DDS_HANDS * DDS_SUITS)]
102
+
103
+
104
+ class ddTableDeals(Structure):
105
+ _fields_ = [
106
+ ("noOfTables", c_int),
107
+ ("deals", ddTableDeal * (MAXNOOFBOARDS >> 2)),
108
+ ]
109
+
110
+
111
+ class ddTableDealPBN(Structure):
112
+ _fields_ = [("cards", c_char * 80)]
113
+
114
+
115
+ class ddTableDealsPBN(Structure):
116
+ _fields_ = [
117
+ ("noOfTables", c_int),
118
+ ("deals", ddTableDealPBN * (MAXNOOFBOARDS >> 2)),
119
+ ]
120
+
121
+
122
+ class ddTableResults(Structure):
123
+ # _fields_ = [("resTable", c_int * DDS_STRAINS * DDS_HANDS)]
124
+ _fields_ = [("resTable", c_int * DDS_HANDS * DDS_STRAINS)]
125
+
126
+
127
+ class ddTablesRes(Structure):
128
+ _fields_ = [
129
+ ("noOfBoards", c_int),
130
+ ("results", ddTableResults * (MAXNOOFBOARDS >> 2)),
131
+ ]
132
+
133
+
134
+ class parResults(Structure):
135
+ """index = 0 is NS view and index = 1
136
+ is EW view. By 'view' is here meant
137
+ which side that starts the bidding."""
138
+
139
+ _fields_ = [
140
+ ("parScore", ((c_char * 16) * 2)),
141
+ ("parContractsString", ((c_char * 128) * 2)),
142
+ ]
143
+
144
+
145
+ class allParResults(Structure):
146
+ _fields_ = [("presults", parResults * MAXNOOFBOARDS)]
147
+
148
+
149
+ class parResultsDealer(Structure):
150
+ _fields_ = [
151
+ ("number", c_int),
152
+ ("score", c_int),
153
+ ("contracts", c_char * 10 * 10),
154
+ ]
155
+
156
+
157
+ class contractType(Structure):
158
+ """undertricks: 0 = make; 1-13 = sacrifice
159
+ overTricks: 0-3; e.g. 1 for 4S + 1
160
+ level: 1-7
161
+ denom: 0 = No Trumps, 1 = trump Spades, 2 = trump Hearts
162
+ 3 = trump Diamonds, 4 = trump Clubs
163
+ seats: One of the cases N, E, S, W, NS, EW;
164
+ 0 = N, 1 = E, 2 = S, 3 = W, 4 = NS, 5 = EW"""
165
+
166
+ _fields_ = [
167
+ ("underTricks", c_int),
168
+ ("overTricks", c_int),
169
+ ("level", c_int),
170
+ ("denom", c_int),
171
+ ("seats", c_int),
172
+ ]
173
+
174
+
175
+ class parResultsMaster(Structure):
176
+ """score: Sign acccording to NS iew
177
+ number: Number of contracts giving the par score"""
178
+
179
+ _fields_ = [
180
+ ("score", c_int),
181
+ ("number", c_int),
182
+ ("contracts", contractType * 10),
183
+ ]
184
+
185
+
186
+ class parTextResults(Structure):
187
+ """parText: Short text for par information, e.g.
188
+ Par -110: EW 2S EW 2D+1
189
+ equal: TRUE in the normal case when it does not matter who
190
+ starts the bidding. Otherwise, FALSE."""
191
+
192
+ _fields_ = [("parTextResults", c_char * 2 * 128), ("equal", c_int)]
193
+
194
+
195
+ class playTraceBin(Structure):
196
+ _fields_ = [("number", c_int), ("suit", c_int * 52), ("rank", c_int * 52)]
197
+
198
+
199
+ class playTracePBN(Structure):
200
+ _fields_ = [("number", c_int), ("cards", c_char * 106)]
201
+
202
+
203
+ class solvedPlay(Structure):
204
+ _fields_ = [("number", c_int), ("tricks", c_int * 53)]
205
+
206
+
207
+ class playTracesBin(Structure):
208
+ _fields_ = [
209
+ ("noOfBoards", c_int),
210
+ ("plays", playTraceBin * (MAXNOOFBOARDS // 10)),
211
+ ]
212
+
213
+
214
+ class playTracesPBN(Structure):
215
+ _fields_ = [
216
+ ("noOfBoards", c_int),
217
+ ("plays", playTracePBN * (MAXNOOFBOARDS // 10)),
218
+ ]
219
+
220
+
221
+ class solvedPlays(Structure):
222
+ _fields_ = [
223
+ ("noOfBoards", c_int),
224
+ ("solved", solvedPlay * (MAXNOOFBOARDS // 10)),
225
+ ]
226
+
227
+
228
+ SetMaxThreads = dds.SetMaxThreads
229
+ """int userThreads"""
230
+ SetMaxThreads.argtypes = [c_int]
231
+ SetMaxThreads.restype = None
232
+
233
+ FreeMemory = dds.FreeMemory
234
+ FreeMemory.argtypes = None
235
+ FreeMemory.restype = None
236
+
237
+ SolveBoard = dds.SolveBoard
238
+ """deal dl
239
+ int target
240
+ int solutions
241
+ int mode,
242
+ pointer to struct futureTricks * futp
243
+ int threadIndex"""
244
+ SolveBoard.argtypes = [deal, c_int, c_int, c_int, POINTER(futureTricks), c_int]
245
+ SolveBoard.restype = c_int
246
+
247
+ SolveBoardPBN = dds.SolveBoardPBN
248
+ """dealPBN dlpbn
249
+ int target
250
+ int solutions
251
+ int mode
252
+ pointer to struct futureTricks * futp
253
+ int thrId"""
254
+ SolveBoardPBN.argtypes = [
255
+ dealPBN,
256
+ c_int,
257
+ c_int,
258
+ c_int,
259
+ POINTER(futureTricks),
260
+ c_int,
261
+ ]
262
+ SolveBoardPBN.restype = c_int
263
+
264
+ CalcDDtable = dds.CalcDDtable
265
+ """struct ddTableDeal tableDeal
266
+ pointer to struct ddTableResults * tablep"""
267
+ CalcDDtable.argtypes = [ddTableDeal, POINTER(ddTableResults)]
268
+ CalcDDtable.restype = c_int
269
+
270
+ CalcDDtablePBN = dds.CalcDDtablePBN
271
+ """srtuct ddTableDealPBN tableDealPBN
272
+ pointer to struct ddTableResults * tablep"""
273
+ CalcDDtablePBN.argtypes = [ddTableDealPBN, POINTER(ddTableResults)]
274
+ CalcDDtablePBN.restype = c_int
275
+
276
+ CalcAllTables = dds.CalcAllTables
277
+ """pointer to struct dd TableDeals * dealsp
278
+ int mode
279
+ int trumpFilter[DDS_STRAINS]
280
+ poiter to struct ddTablesRes * resp
281
+ pointer to struct allParResults'* presp"""
282
+ CalcAllTables.argtypes = [
283
+ POINTER(ddTableDeals),
284
+ c_int,
285
+ c_int * DDS_STRAINS,
286
+ POINTER(ddTablesRes),
287
+ POINTER(allParResults),
288
+ ]
289
+ CalcAllTables.restype = c_int
290
+
291
+ CalcAllTablesPBN = dds.CalcAllTablesPBN
292
+ """pointer to struct ddTableDealsPBN * dealsp
293
+ int mode
294
+ int trumpFilter[DDS_STRINS]
295
+ pointer to struct ddTablesRes *resp
296
+ pointer to struct allParResults * presp"""
297
+ CalcAllTablesPBN.argtypes = [
298
+ POINTER(ddTableDealsPBN),
299
+ c_int,
300
+ c_int * DDS_STRAINS,
301
+ POINTER(ddTablesRes),
302
+ POINTER(allParResults),
303
+ ]
304
+ CalcAllTablesPBN.restype = c_int
305
+
306
+ SolveAllBoards = dds.SolveAllBoards
307
+ """pointer to struct boardsPBN * bop
308
+ pointer to struct solvedBoards * solvedp"""
309
+ SolveAllBoards.argtypes = [POINTER(boardsPBN), POINTER(solvedBoards)]
310
+ SolveAllBoards.restype = c_int
311
+
312
+ SolveAllChunks = dds.SolveAllChunks
313
+ """pointer to struct boardsPBN * bop
314
+ pointer to struct solvedBoards * solvedP
315
+ int chunkSize"""
316
+ SolveAllChunks.argtypes = [POINTER(boardsPBN), POINTER(solvedBoards), c_int]
317
+ SolveAllChunks.restype = c_int
318
+
319
+ solveAllChunksBin = dds.SolveAllChunksBin
320
+ """pointer to struct boards * bop
321
+ pointer to struct solvedBoards * solvedp
322
+ int chunkSize"""
323
+ solveAllChunksBin.argtypes = [POINTER(boards), POINTER(solvedBoards), c_int]
324
+ solveAllChunksBin.restype = c_int
325
+
326
+ solveAllChunksPBN = dds.SolveAllChunksPBN
327
+ """pointer to struct boardsPBN * bop
328
+ pointer to struct solvedBoards * solvedp
329
+ int chunkSize"""
330
+ solveAllChunksPBN.argtypes = [POINTER(boardsPBN), POINTER(solvedBoards), c_int]
331
+ solveAllChunksPBN.restype = c_int
332
+
333
+ SolveAllChunksPBN = dds.SolveAllChunksPBN
334
+ """pointer to struct boardsPBN * bop
335
+ pointer to struct solvedBoards * solvedp
336
+ int chunkSize"""
337
+ SolveAllChunksPBN.argtypes = [POINTER(boardsPBN), POINTER(solvedBoards), c_int]
338
+ SolveAllChunksPBN.restype = c_int
339
+
340
+ Par = dds.Par
341
+ """pointer to struct ddTableResults * tablep
342
+ pointer to struct parResults * presp
343
+ int vulnerable"""
344
+ Par.argtypes = [POINTER(ddTableResults), POINTER(parResults), c_int]
345
+ Par.restype = c_int
346
+
347
+ CalcPar = dds.CalcPar
348
+ """struct ddTableDeal
349
+ int ulnerable
350
+ pointer to struct ddTablesRes * tablep
351
+ pointer to parResults * presp"""
352
+ CalcPar.argtypes = [
353
+ ddTableDeal,
354
+ c_int,
355
+ POINTER(ddTableResults),
356
+ POINTER(parResults),
357
+ ]
358
+ CalcPar.restype = c_int
359
+
360
+ CalcPar = dds.CalcPar
361
+ """struct ddTableDeal tableDeal
362
+ int vulnerable
363
+ pointer to struct ddTableResults * tablep
364
+ pointer to parResults * presp"""
365
+ CalcPar.argtypes = [
366
+ ddTableDeal,
367
+ c_int,
368
+ POINTER(ddTableResults),
369
+ POINTER(parResults),
370
+ ]
371
+ CalcPar.restype = c_int
372
+
373
+ CalcParPBN = dds.CalcParPBN
374
+ """struct ddTableDealPBN tableDealPBN
375
+ pointer tostruct ddTableResults * tablep
376
+ int vulnerable
377
+ pointer to struct parResults * presp"""
378
+ CalcParPBN.argtypes = [
379
+ ddTableDealPBN,
380
+ POINTER(ddTableResults),
381
+ c_int,
382
+ POINTER(parResults),
383
+ ]
384
+ CalcParPBN.restype = c_int
385
+
386
+ SidesPar = dds.SidesPar
387
+ """pointer to struct ddTableResults * tablep,
388
+ array struct parResultsDealer sidesRes[2],
389
+ int vulnerable"""
390
+ SidesPar.argtypes = [POINTER(ddTableResults), parResultsDealer * 2, c_int]
391
+ SidesPar.restypes = c_int
392
+
393
+ DealerPar = dds.DealerPar
394
+ """pointer to struct ddTableResults * tablep
395
+ pointer to struct parResultsDealer * presp
396
+ int dealer
397
+ int vulnerable"""
398
+ DealerPar.argtypes = [
399
+ POINTER(ddTableResults),
400
+ POINTER(parResultsDealer),
401
+ c_int,
402
+ c_int,
403
+ ]
404
+ DealerPar.restype = c_int
405
+
406
+ DealerParBin = dds.DealerParBin
407
+ """pointer to struct ddTableResults * tablep
408
+ pointer to struct parResultsMaster * presp
409
+ int dealer
410
+ int vulnerable"""
411
+ DealerParBin.argtypes = [
412
+ POINTER(ddTableResults),
413
+ POINTER(parResultsMaster),
414
+ c_int,
415
+ c_int,
416
+ ]
417
+ DealerParBin.restype = c_int
418
+
419
+ SidesParBin = dds.SidesParBin
420
+ """pointer to struct ddTableResults * tablep
421
+ array struct parResultsMaster sidesRes[2]
422
+ int vulnerable"""
423
+ SidesParBin.argtypes = [POINTER(ddTableResults), parResultsMaster * 2, c_int]
424
+ SidesParBin.restype = c_int
425
+
426
+ ConvertToDealerTextFormat = dds.ConvertToDealerTextFormat
427
+ """pointer to struct parResultsMaster *pres
428
+ pointer to char *resp"""
429
+ ConvertToDealerTextFormat.argtypes = [POINTER(parResultsMaster), c_char_p]
430
+ ConvertToDealerTextFormat.restype = c_int
431
+
432
+ ConvertToSidesTextFormat = dds.ConvertToSidesTextFormat
433
+ """pointer to struct parResultsMaster * pres,
434
+ pointer to struct parTextResults * resp"""
435
+ ConvertToSidesTextFormat.argtypes = [
436
+ POINTER(parResultsMaster),
437
+ POINTER(parTextResults),
438
+ ]
439
+ ConvertToSidesTextFormat.restype = c_int
440
+
441
+ AnalysePlayBin = dds.AnalysePlayBin
442
+ """struct deal dl
443
+ struct playTraceBin play
444
+ pointer to struct solvedPlay * solved
445
+ int thrId"""
446
+ AnalysePlayBin.argtypes = [deal, playTraceBin, POINTER(solvedPlay), c_int]
447
+ AnalysePlayBin.restype = c_int
448
+
449
+ AnalysePlayPBN = dds.AnalysePlayPBN
450
+ """struct dealPBN dlPBN
451
+ struct playTracePBN playPBN
452
+ pointer to struct solvedPlay * solvedp
453
+ int thrId"""
454
+ AnalysePlayPBN.argtypes = [dealPBN, playTracePBN, POINTER(solvedPlay), c_int]
455
+ AnalysePlayPBN.restype = c_int
456
+
457
+ AnalyseAllPlaysBin = dds.AnalyseAllPlaysBin
458
+ """pointer to struct boards * bop
459
+ pointer to struct playTracesBin * plp
460
+ pointer to struct solvedPlays * solvedp
461
+ int chunkSize"""
462
+ AnalyseAllPlaysBin.argtypes = [
463
+ POINTER(boards),
464
+ POINTER(playTracesBin),
465
+ POINTER(solvedPlays),
466
+ c_int,
467
+ ]
468
+ AnalyseAllPlaysBin.restype = c_int
469
+
470
+ AnalyseAllPlaysPBN = dds.AnalyseAllPlaysPBN
471
+ """pointer to struct boardsPBN * bopPBN
472
+ pointer to struct playTracesPBN * plpPBN
473
+ pointer to struct solvedPlays * solvedp
474
+ int chunkSize"""
475
+ AnalyseAllPlaysPBN.argtypes = [
476
+ POINTER(boardsPBN),
477
+ POINTER(playTracesPBN),
478
+ POINTER(solvedPlays),
479
+ c_int,
480
+ ]
481
+ AnalyseAllPlaysPBN.restype = c_int
482
+
483
+ ErrorMessage = dds.ErrorMessage
484
+ """int code
485
+ char * 80"""
486
+ ErrorMessage.argtypes = [c_int, POINTER(c_char)]
487
+ ErrorMessage.restype = c_int
gui_app.py ADDED
@@ -0,0 +1,1094 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ import queue
4
+ import threading
5
+ import tkinter as tk
6
+ from ctypes import c_int, c_long, pointer, string_at
7
+ from datetime import datetime
8
+ from tkinter import filedialog, messagebox, ttk
9
+
10
+ import cv2
11
+ import numpy as np
12
+ from PIL import Image, ImageTk
13
+ from transformers import pipeline
14
+
15
+ import dds
16
+ from identify_cards import (determine_and_correct_orientation, find_center_box,
17
+ find_rank_candidates, get_suit_from_color_rules,
18
+ get_suit_from_image_rules, load_suit_templates,
19
+ recognize_cards, save_img_with_rect)
20
+ from utils import (PrintTable, arrange_hand, convert2ddTableDeal, convert2dup,
21
+ convert2pbn, convert2pbn_board, convert2pbn_txt,
22
+ convert2xhd, convert_hands_to_binary_deal, is_text_valid,
23
+ reshape_table)
24
+
25
+ # --- グローバル変数・設定 ---
26
+ # print("TrOCRのAIモデルを読み込んでいます...(初回は数分かかります)")
27
+ # try:
28
+ # trocr_pipeline = pipeline(
29
+ # "image-to-text", model="microsoft/trocr-base-printed"
30
+ # )
31
+ # print("TrOCRの準備が完了しました。")
32
+ # except Exception as e:
33
+ # print(f"TrOCRモデルのロード中にエラー: {e}")
34
+ # trocr_pipeline = None
35
+
36
+ DEFAULT_THRESHOLDS = {
37
+ "L_black": 65.0,
38
+ "a_green": 126.0,
39
+ "a_black": 120.0, # 250.0, # 120.0,
40
+ # "a_black2": 126.0,
41
+ "a_red": 134.0,
42
+ "b_orange": 137.0,
43
+ "ba_green": 0.0,
44
+ "ba_black": -4.5,
45
+ "ab_black": 250.0,
46
+ "b_black": 121.3,
47
+ "b_black2": 132.5,
48
+ # "b_black2": 127.0,
49
+ "a_orange": 150.0,
50
+ "b_red": 145.0,
51
+ "a_b_red": 9.0,
52
+ }
53
+ SUITS_BY_COLOR = {
54
+ "black": "S",
55
+ "green": "C",
56
+ "red": "H",
57
+ "orange": "D",
58
+ "unknown": "*",
59
+ }
60
+ PLAYER_ORDER = ["north", "south", "west", "east"]
61
+ MARGIN = 200
62
+ SUIT_TEMPLATE_PATH = "templates/suits/"
63
+
64
+
65
+
66
+
67
+
68
+ def validate_deal(hands):
69
+ if not hands:
70
+ return False, "分析対象のカードデータがありません"
71
+ total_cards = []
72
+
73
+ # 手札が合計52枚あるかチェック
74
+ for player, hand in hands.items():
75
+ if len(hand) != 13:
76
+ return (
77
+ False,
78
+ f"エラー: {player.capitalize()}の手札が13枚ではありません",
79
+ )
80
+ for card in hand:
81
+ if card in total_cards:
82
+ return (
83
+ False,
84
+ f"エラー: 重複したカードが検出されました ({card})",
85
+ )
86
+ total_cards.append(card)
87
+
88
+ return True, "デックは正常です"
89
+
90
+
91
+ def load_image(path):
92
+ try:
93
+ # ファイルをバイナリモードで安全に読み込む
94
+ with open(path, "rb") as f:
95
+ # バイトデータをNumPy配列に変換
96
+ file_bytes = np.asarray(bytearray(f.read()), dtype=np.uint8)
97
+ # NumPy配列(メモリ上のデータ)から画像をデコード
98
+ image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
99
+
100
+ if image is None:
101
+ raise ValueError(
102
+ "OpenCVが画像をデコードできませんでした。ファイルが破損しているか、非対応の形式の可能性があります。"
103
+ )
104
+ return image, ""
105
+ # image_objects[filename] = image
106
+ except Exception as e:
107
+ # ファイル読み込み自体のエラーをキャッチ
108
+
109
+ # image_objects[filename] = None
110
+ return None, e
111
+
112
+
113
+ def get_player_region_image(image, box):
114
+ bx, by, bw, bh = box
115
+ h, w, _ = image.shape
116
+ player_regions = {
117
+ "north": image[0:by, :],
118
+ "south": image[by + bh : h, :],
119
+ "west": image[by - MARGIN : by + bh + MARGIN, 0:bx],
120
+ "east": image[by - MARGIN : by + bh + MARGIN, bx + bw : w],
121
+ }
122
+
123
+ # 向きの補正
124
+ for player, region in player_regions.items():
125
+ if region is not None and region.size > 0:
126
+ if player == "north":
127
+ player_regions[player] = cv2.rotate(region, cv2.ROTATE_180)
128
+ elif player == "east":
129
+ player_regions[player] = cv2.rotate(
130
+ region, cv2.ROTATE_90_CLOCKWISE
131
+ )
132
+ elif player == "west":
133
+ player_regions[player] = cv2.rotate(
134
+ region, cv2.ROTATE_90_COUNTERCLOCKWISE
135
+ )
136
+
137
+ return player_regions
138
+
139
+
140
+ def analyze_image_data(image_paths, progress_queue, trocr_pipeline):
141
+ all_results = []
142
+ num_total_files = len(image_paths)
143
+
144
+ progress_queue.put("テンプレート画像読み込み中...")
145
+ suit_templates = load_suit_templates(SUIT_TEMPLATE_PATH)
146
+ if not suit_templates:
147
+ raise ValueError(
148
+ f"エラー: {SUIT_TEMPLATE_PATH} フォルダにスートのテンプレート画像が見つかりま��ん。"
149
+ )
150
+
151
+ try:
152
+ progress_queue.put("ステージ1/3: 文字候補を検出中...")
153
+ all_candidates_global = []
154
+ # image_objects = {}
155
+
156
+ for i, image_path in enumerate(image_paths):
157
+ filename = os.path.basename(image_path)
158
+ progress_queue.put(f"分析中 ({i+1}/{num_total_files}): {filename}")
159
+ image, error = load_image(image_path)
160
+ if image is None:
161
+ all_results.append(
162
+ {
163
+ "filename": filename,
164
+ "error": f"画像読み込みエラー: {error}",
165
+ }
166
+ )
167
+
168
+ # box = find_center_box(image)
169
+ print("detect board")
170
+ rotated_image, box, scale = determine_and_correct_orientation(
171
+ image, progress_queue.put
172
+ )
173
+ if box is None:
174
+ all_results.append(
175
+ {"filename": filename, "error": "中央ボードの検出に失敗"}
176
+ )
177
+ continue
178
+ print(box)
179
+
180
+ save_img_with_rect("debug_rotated.jpg", rotated_image, [box])
181
+ player_regions = get_player_region_image(rotated_image, box)
182
+
183
+ for player, region in player_regions.items():
184
+ candidates = find_rank_candidates(
185
+ region, suit_templates, player, scale
186
+ )
187
+ for cand in candidates:
188
+ cand["filename"] = filename
189
+ cand["player"] = player
190
+ all_candidates_global.append(cand)
191
+
192
+ progress_queue.put(
193
+ "ステージ2/3: 文字認識を実行中... (時間がかかります)"
194
+ )
195
+
196
+ if not all_candidates_global or not trocr_pipeline:
197
+ progress_queue.put("認識する文字候補がありませんでした。")
198
+ progress_queue.put(
199
+ all_results
200
+ ) # エラーがあった画像の結果だけを返す
201
+ return
202
+
203
+ candidates_pil_images = [
204
+ Image.fromarray(cv2.cvtColor(c["img"], cv2.COLOR_BGR2RGB))
205
+ for c in all_candidates_global
206
+ ]
207
+ ocr_results = trocr_pipeline(candidates_pil_images)
208
+
209
+ # --- ステージ3: 結果の仕分けと最終的なカードの特定 ---
210
+ progress_queue.put("ステージ3/3: 認識結果を仕分け中...")
211
+
212
+ # まず、ファイルごとに結果を格納する辞書を準備
213
+ temp_results = {
214
+ os.path.basename(p): {player: [] for player in PLAYER_ORDER}
215
+ for p in image_paths
216
+ }
217
+ print(temp_results)
218
+ print([result[0]["generated_text"] for result in ocr_results])
219
+
220
+ raw_data = []
221
+ blacks = []
222
+ reds = []
223
+ for i, result in enumerate(ocr_results):
224
+ text = result[0]["generated_text"].upper().strip()
225
+ print(text, is_text_valid(text))
226
+
227
+ text = is_text_valid(text)
228
+ if text is not None:
229
+ candidate_info = all_candidates_global[i]
230
+ print(
231
+ f"--- 診断中: ランク '{text}' of {candidate_info['player']} at {candidate_info['pos']} with thick:{candidate_info["thickness"]} ---"
232
+ )
233
+ color_name, avg_lab = get_suit_from_image_rules(
234
+ candidate_info["no_pad"], DEFAULT_THRESHOLDS
235
+ )
236
+ if color_name == "black" or color_name == "green":
237
+ blacks.append(
238
+ 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}"
239
+ )
240
+ if color_name == "red" or color_name == "orange":
241
+ reds.append(
242
+ 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}"
243
+ )
244
+ print(color_name)
245
+ if color_name == "mark":
246
+ continue
247
+ candidate_info["avg_lab"] = avg_lab
248
+ candidate_info["color"] = color_name
249
+ candidate_info["name"] = text
250
+ raw_data.append(candidate_info)
251
+ if color_name in SUITS_BY_COLOR:
252
+ suit = SUITS_BY_COLOR[color_name]
253
+ card_name = f"{suit}{text}"
254
+
255
+ filename = os.path.basename(candidate_info["filename"])
256
+ player = candidate_info["player"]
257
+ temp_results[filename][player].append(card_name)
258
+
259
+ print("\r\n".join(blacks))
260
+ # print("\r\n".join(reds))
261
+ progress_queue.put(raw_data)
262
+
263
+ except Exception as e:
264
+ progress_queue.put(f"致命的なエラー: {e}")
265
+
266
+
267
+ def convert2txt(all_result, title):
268
+ res = ""
269
+ for result in all_result:
270
+ res += "=" * 40 + "\n"
271
+ res += f"{title}\n"
272
+ res += "=" * 40 + "\n"
273
+ if "error" in result:
274
+ res += f" エラー: {result['error']}\n"
275
+ elif "hands" in result:
276
+ for player in PLAYER_ORDER:
277
+ hand = result["hands"].get(player, [])
278
+ res += f" {player.capitalize()}: {', '.join(hand) if hand else '(なし)'}\n"
279
+ res += "\n"
280
+ return res
281
+
282
+
283
+ def parse_hand_string(hand):
284
+ hand_list = hand.replace(" ", "").split(",")
285
+ return hand_list
286
+
287
+
288
+ XHD = "xhdファイル"
289
+ DUP = "dupファイル"
290
+ PBN = "pbnファイル"
291
+
292
+
293
+ # --- Tkinter GUI アプリケーションクラス ---
294
+ class CardRecognizerApp:
295
+ def __init__(self, root):
296
+ self.root = root
297
+ self.root.title("トランプカード認識アプリ")
298
+ self.root.geometry("800x600")
299
+
300
+ self.filepaths = []
301
+ self.all_result = []
302
+ self.thread = None
303
+ self.q = queue.Queue()
304
+ self.format_var = tk.StringVar()
305
+ self.raw_rank_data = []
306
+
307
+ self.trocr_pipeline = None
308
+ self.model_loading_thread = None
309
+ self.model_loaded = threading.Event()
310
+ self.model_load_error = None
311
+
312
+ self.selected_deal_var = tk.StringVar()
313
+
314
+ self.setup_ui()
315
+ self.start_model_loading()
316
+
317
+ def setup_ui(self):
318
+ # スタイル
319
+ style = ttk.Style()
320
+ style.configure(
321
+ "TButton", padding=6, relief="flat", font=("Yu Gothic UI", 10)
322
+ )
323
+ style.configure("TLabel", padding=5, font=("Yu Gothic UI", 10))
324
+ style.configure("Header.TLabel", font=("Yu Gothic UI", 14, "bold"))
325
+
326
+ # メインフレーム
327
+ main_frame = ttk.Frame(root, padding="20")
328
+ main_frame.pack(fill=tk.BOTH, expand=True)
329
+ top_frame = ttk.Frame(main_frame)
330
+ top_frame.pack(fill=tk.X, pady=10)
331
+
332
+ # ファイル選択部分
333
+ file_frame = ttk.Frame(main_frame)
334
+ file_frame.pack(fill=tk.X, pady=10)
335
+
336
+ self.select_button = ttk.Button(
337
+ file_frame, text="画像ファイルを選択", command=self.select_files
338
+ )
339
+ self.select_button.pack(side=tk.LEFT, padx=5)
340
+
341
+ self.filepath_label = ttk.Label(
342
+ file_frame, text="ファイルが選択されていません", anchor=tk.W
343
+ )
344
+ self.filepath_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
345
+
346
+ nav_frame = ttk.Frame(main_frame)
347
+ nav_frame.pack(fill=tk.X, pady=5)
348
+ ttk.Label(nav_frame, text="表示中のディール:").pack(
349
+ side=tk.LEFT, padx=(0, 5)
350
+ )
351
+ self.deal_selector_combo = ttk.Combobox(
352
+ nav_frame,
353
+ textvariable=self.selected_deal_var,
354
+ state="disabled",
355
+ width=50,
356
+ )
357
+ self.deal_selector_combo.pack(side=tk.LEFT)
358
+ self.deal_selector_combo.bind(
359
+ "<<ComboboxSelected>>", self.on_deal_selected
360
+ )
361
+
362
+ # 分析開始ボタン
363
+ self.analyze_button = ttk.Button(
364
+ main_frame,
365
+ text="分析開始",
366
+ command=self.start_analysis_thread,
367
+ state=tk.DISABLED,
368
+ )
369
+ self.analyze_button.pack(pady=10, fill=tk.X)
370
+ self.export_button = ttk.Button(
371
+ top_frame,
372
+ text="結果を出力",
373
+ command=self.export_results,
374
+ state=tk.DISABLED,
375
+ )
376
+ self.export_button.pack(side=tk.LEFT, padx=5)
377
+
378
+ self.dds_single_button = ttk.Button(
379
+ top_frame,
380
+ text="現在のボードをDDS分析",
381
+ command=self.start_dds_single_analysis,
382
+ state=tk.DISABLED,
383
+ )
384
+ self.dds_single_button.pack(side=tk.LEFT, padx=(15, 5))
385
+ self.dds_button = ttk.Button(
386
+ top_frame,
387
+ text="全てのファイルをDDS分析",
388
+ command=self.start_dds_analysis,
389
+ state=tk.DISABLED,
390
+ )
391
+ self.dds_button.pack(side=tk.LEFT, padx=5)
392
+
393
+ self.debugger_button = ttk.Button(
394
+ top_frame,
395
+ text="カラーデバッガー",
396
+ command=self.open_debugger,
397
+ state=tk.DISABLED,
398
+ )
399
+ self.debugger_button.pack(side=tk.LEFT, padx=5)
400
+
401
+ # 結果表示部分
402
+ results_frame = ttk.Frame(main_frame, padding="10")
403
+ results_frame.pack(fill=tk.BOTH, expand=True, pady=10)
404
+ results_frame.columnconfigure(1, weight=1)
405
+
406
+ self.north_hand_var = tk.StringVar(results_frame)
407
+ self.south_hand_var = tk.StringVar(results_frame)
408
+ self.west_hand_var = tk.StringVar(results_frame)
409
+ self.east_hand_var = tk.StringVar(results_frame)
410
+ self.player_vars = {
411
+ "north": self.north_hand_var,
412
+ "south": self.south_hand_var,
413
+ "west": self.west_hand_var,
414
+ "east": self.east_hand_var,
415
+ }
416
+
417
+ self.result_entries = {}
418
+ ttk.Label(
419
+ results_frame, text="最終分析結果:", style="Header.TLabel"
420
+ ).grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=10)
421
+ for i, player in enumerate(PLAYER_ORDER, 1):
422
+ ttk.Label(
423
+ results_frame,
424
+ text=f"{player.capitalize()}:",
425
+ style="Header.TLabel",
426
+ ).grid(row=i, column=0, sticky=tk.NW, padx=5, pady=5)
427
+ entry = ttk.Entry(
428
+ results_frame, textvariable=self.player_vars[player]
429
+ )
430
+ entry.grid(row=i, column=1, sticky=tk.EW, padx=5, pady=5)
431
+ self.result_entries[player] = entry
432
+ # self.result_labels[player] = ttk.Label(
433
+ # results_frame,
434
+ # text="-",
435
+ # wraplength=550,
436
+ # anchor=tk.W,
437
+ # justify=tk.LEFT,
438
+ # )
439
+ # self.result_labels[player].grid(
440
+ # row=i, column=1, sticky=tk.NW, padx=5, pady=5
441
+ # )
442
+
443
+ # ステータスバー
444
+ self.status_var = tk.StringVar()
445
+ self.status_var.set("AIモデルを初期化中...")
446
+ status_bar = ttk.Label(
447
+ root,
448
+ textvariable=self.status_var,
449
+ relief=tk.SUNKEN,
450
+ anchor=tk.W,
451
+ padding=5,
452
+ )
453
+ status_bar.pack(side=tk.BOTTOM, fill=tk.X)
454
+
455
+ def start_model_loading(self):
456
+ self.model_loading_thread = threading.Thread(
457
+ target=self.initialize_model
458
+ )
459
+ self.model_loading_thread.demon = True
460
+ self.model_loading_thread.start()
461
+ self.root.after(100, self.check_model_status)
462
+
463
+ def initialize_model(self):
464
+ """バックグラウンドで実行されるAIモデル読み込み処理"""
465
+ try:
466
+ print("TrOCRのAIモデルを読み込んでいます...")
467
+ self.trocr_pipeline = pipeline(
468
+ "image-to-text", model="microsoft/trocr-small-printed"
469
+ )
470
+ print("TrOCRの準備が完了しました。")
471
+ except Exception as e:
472
+ self.model_load_error = e
473
+ finally:
474
+ self.model_loaded.set() # 読み込み完了(成功または失敗)を通知
475
+
476
+ def check_model_status(self):
477
+ """モデルの読み込みが完了したか定期的にチェックする"""
478
+ if not self.model_loaded.is_set():
479
+ # まだロード中なら100ms後にもう一度チェック
480
+ self.root.after(100, self.check_model_status)
481
+ return
482
+
483
+ if self.model_load_error:
484
+ # エラーが発生した場合
485
+ messagebox.showerror(
486
+ "起動エラー",
487
+ f"TrOCRモデルの読み込みに失敗しました。\nエラー: {self.model_load_error}",
488
+ )
489
+ self.status_var.set("AIモデルの読み込みに失敗しました。")
490
+ else:
491
+ # 成功した場合
492
+ self.status_var.set("準備完了")
493
+ self.filepath_label.config(text="ファイルが選択されていません")
494
+ if len(self.filepaths) > 0:
495
+ self.analyze_button.config(state=tk.NORMAL) # ボタンを有効化
496
+
497
+ def select_files(self):
498
+ self.filepaths = filedialog.askopenfilenames(
499
+ title="画像ファイルを選択 (複数選択可)",
500
+ filetypes=(
501
+ ("JPEGファイル", "*.jpg;*.jpeg"),
502
+ ("PNGファイル", "*.png"),
503
+ ("すべてのファイル", "*.*"),
504
+ ),
505
+ )
506
+ if self.filepaths:
507
+ if len(self.filepaths) == 1:
508
+ self.filepath_label.config(
509
+ text=os.path.basename(self.filepaths[0])
510
+ )
511
+ else:
512
+ self.filepath_label.config(
513
+ text=f"{len(self.filepaths)}個のファイルが選択されました"
514
+ )
515
+ if self.trocr_pipeline is not None:
516
+ self.analyze_button.config(state=tk.NORMAL)
517
+ self.export_button.config(state=tk.DISABLED)
518
+ self.status_var.set(f"{len(self.filepaths)}個のファイルを選択")
519
+
520
+ def start_analysis_thread(self):
521
+ if not self.filepaths:
522
+ return
523
+
524
+ self.analyze_button.config(state=tk.DISABLED)
525
+ self.select_button.config(state=tk.DISABLED)
526
+ self.export_button.config(state=tk.DISABLED)
527
+ self.dds_button.config(state=tk.DISABLED)
528
+ self.dds_single_button.config(state=tk.DISABLED)
529
+ self.all_result = []
530
+ self.deal_selector_combo.set("")
531
+ self.deal_selector_combo.config(state=tk.DISABLED)
532
+
533
+ self.status_var.set("分析処理を開始します...")
534
+
535
+ # 以前の結果をクリア
536
+ for player in PLAYER_ORDER:
537
+ # self.result_labels[player].config(text="-")
538
+ self.player_vars[player] = ""
539
+ self.result_entries[player].delete(0, tk.END)
540
+
541
+ # 分析処理を別スレッドで実行
542
+ self.thread = threading.Thread(
543
+ target=analyze_image_data,
544
+ args=(self.filepaths, self.q, self.trocr_pipeline),
545
+ )
546
+ self.thread.daemon = True
547
+ self.thread.start()
548
+
549
+ # キューを監視する
550
+ self.root.after(100, self.process_queue)
551
+
552
+ def process_queue(self):
553
+ try:
554
+ msg = self.q.get_nowait()
555
+ if isinstance(msg, list):
556
+ # 最終結果が来た場合
557
+ self.raw_rank_data = msg
558
+ # self.all_result = msg
559
+ # self.display_last_results()
560
+ self.status_var.set("分析完了!")
561
+ self.analyze_button.config(state=tk.NORMAL)
562
+ self.select_button.config(state=tk.NORMAL)
563
+ self.arrange_data()
564
+ self.setup_deal_selector()
565
+ if self.raw_rank_data:
566
+ self.debugger_button.config(state=tk.NORMAL)
567
+ if self.all_result:
568
+ self.export_button.config(state=tk.NORMAL)
569
+ if any("hands" in result for result in self.all_result):
570
+ self.dds_button.config(state=tk.NORMAL)
571
+ else:
572
+ # 途中の進捗メッセージの場合
573
+ self.status_var.set(msg)
574
+ self.root.after(
575
+ 100, self.process_queue
576
+ ) # 次のメッセージをチェック
577
+ except queue.Empty:
578
+ # キューが空なら、再度チェック
579
+ self.root.after(100, self.process_queue)
580
+
581
+ def arrange_data(self):
582
+ # 生データから最終的な手札を作成・表示
583
+ self.all_result = []
584
+ temp_hands = {} # ファイルごとの手札を一時保存
585
+
586
+ for rank_data in self.raw_rank_data:
587
+ filename = rank_data["filename"]
588
+ if filename not in temp_hands:
589
+ temp_hands[filename] = {p: [] for p in PLAYER_ORDER}
590
+
591
+ color_name = rank_data["color"]
592
+ suit = SUITS_BY_COLOR[color_name]
593
+ card_name = f"{suit}{rank_data['name']}"
594
+ temp_hands[filename][rank_data["player"]].append(card_name)
595
+
596
+ # 整形してall_resultsに格納
597
+ for filename, hands in temp_hands.items():
598
+ self.all_result.append(
599
+ {
600
+ "filename": filename,
601
+ "hands": {
602
+ player: arrange_hand(cards)
603
+ for player, cards in hands.items()
604
+ },
605
+ }
606
+ )
607
+
608
+ def display_last_results(self):
609
+ self.arrange_data()
610
+ if not self.all_result:
611
+ print("all_result is none")
612
+ return
613
+ last_result = self.all_result[-1]
614
+ if "error" in last_result:
615
+ self.filepath_label.config(
616
+ text=f"エラー ({last_result['filename']}): {last_result['error']}"
617
+ )
618
+ elif "hands" in last_result:
619
+ self.filepath_label.config(
620
+ text=f"最終分析ファイル: {last_result['filename']}"
621
+ )
622
+ self.last_analyzed_hands = last_result["hands"]
623
+ self.dds_button.config(state=tk.NORMAL)
624
+ self.dds_single_button.config(state=tk.NORMAL)
625
+
626
+ for player, hand in last_result["hands"].items():
627
+ if player in self.player_vars:
628
+ self.result_entries[player].insert(
629
+ tk.END, ", ".join(hand) if hand else ""
630
+ )
631
+ # self.set_text()
632
+ # self.player_vars[player].set(
633
+ # ", ".join(hand) if hand else ""
634
+ # )
635
+ # self.result_labels[player].config(
636
+ # text=", ".join(hand) if hand else "(なし)"
637
+ # )
638
+
639
+ def setup_deal_selector(self):
640
+ successful_files = [
641
+ r["filename"] for r in self.all_result if "hands" in r
642
+ ]
643
+ if successful_files:
644
+ self.deal_selector_combo["values"] = successful_files
645
+ self.deal_selector_combo.config(state="readonly")
646
+ self.deal_selector_combo.set(successful_files[0])
647
+ self.on_deal_selected()
648
+ else:
649
+ self.filepath_label.config(
650
+ text="分析に成功したディールがありませんでした"
651
+ )
652
+
653
+ def on_deal_selected(self, event=None):
654
+ selected_filename = self.deal_selector_combo.get()
655
+ if not selected_filename:
656
+ return
657
+
658
+ for result in self.all_result:
659
+ if result.get("filename") == selected_filename:
660
+ if "hands" in result:
661
+ self.filepath_label.config(
662
+ text=f"表示中: {result['filename']}"
663
+ )
664
+ for player, hand in result["hands"].items():
665
+ if player in self.player_vars:
666
+ self.result_entries[player].delete(0, tk.END)
667
+ self.result_entries[player].insert(
668
+ tk.END, ", ".join(hand) if hand else ""
669
+ )
670
+ self.dds_single_button.config(state=tk.NORMAL)
671
+ return
672
+
673
+ def start_dds_analysis(self):
674
+ valid_deals = []
675
+ for result in self.all_result:
676
+ if "hands" in result:
677
+ is_valid, _ = validate_deal(result["hands"])
678
+ if is_valid:
679
+ valid_deals.append(result)
680
+
681
+ if len(valid_deals) == 0:
682
+ messagebox.showwarning(
683
+ "分析不可", "分析対象となる正常なディールがありません。"
684
+ )
685
+ return
686
+
687
+ self.status_var.set(f"{len(valid_deals)}件のディールを分析中...")
688
+ self.root.update_idletasks()
689
+
690
+ try:
691
+ deals = dds.ddTableDealsPBN()
692
+ deals.noOfTables = len(valid_deals)
693
+ for i, result in enumerate(valid_deals):
694
+ pbn_deal_string = convert2pbn_txt(result["hands"], "N")
695
+ print(pbn_deal_string)
696
+
697
+ # table_deal_pbn = dds.ddTableDealPBN()
698
+ # table_deal_pbn.cards = pbn_deal_string.encode("utf-8")
699
+
700
+ deals.deals[i].cards = pbn_deal_string.encode("utf-8")
701
+
702
+ dds.SetMaxThreads(0)
703
+ table_res = dds.ddTablesRes()
704
+ per_res = dds.allParResults()
705
+ # table_res_pointer = pointer(table_res)
706
+ res = dds.CalcAllTablesPBN(
707
+ pointer(deals),
708
+ 0,
709
+ (c_int * 5)(0, 0, 0, 0, 0),
710
+ pointer(table_res),
711
+ pointer(per_res),
712
+ )
713
+ print("dds")
714
+
715
+ if res != dds.RETURN_NO_FAULT:
716
+ err_char_p = dds.ErrorMessage(res)
717
+ err_string = (
718
+ string_at(err_char_p).decode("utf-8")
719
+ if err_char_p
720
+ else "Unknown error"
721
+ )
722
+ raise RuntimeError(
723
+ f"DDS Solver failed with code: {res} ({err_string})"
724
+ )
725
+
726
+ filenames = [d["filename"] for d in valid_deals]
727
+
728
+ # 3. 結果を新しいウィンドウで表示
729
+ DDSResultsWindow(self.root, table_res, filenames, False)
730
+ self.status_var.set("ダブルダミー分析が完了しました。")
731
+
732
+ except Exception as e:
733
+ messagebox.showerror(
734
+ "DDS分析エラー", f"分析中にエラーが発生しました:\n{e}"
735
+ )
736
+ self.status_var.set("DDS分析中にエラーが発生しました。")
737
+
738
+ def start_dds_single_analysis(self):
739
+ edited_hands = {
740
+ player: parse_hand_string(var.get())
741
+ for player, var in self.result_entries.items()
742
+ }
743
+
744
+ self.status_var.set("ディールを分析中...")
745
+ self.root.update_idletasks()
746
+
747
+ try:
748
+ print(edited_hands)
749
+ deals = dds.ddTableDealPBN()
750
+ pbn_deal_string = convert2pbn_txt(edited_hands, "N")
751
+ print(pbn_deal_string)
752
+
753
+ # table_deal_pbn = dds.ddTableDealPBN()
754
+ # table_deal_pbn.cards = pbn_deal_string.encode("utf-8")
755
+
756
+ deals.cards = pbn_deal_string.encode("utf-8")
757
+
758
+ table_res = dds.ddTableResults()
759
+ # table_res_pointer = pointer(table_res)
760
+ res = dds.CalcDDtablePBN(deals, table_res)
761
+ print("dds")
762
+
763
+ if res != dds.RETURN_NO_FAULT:
764
+ err_char_p = dds.ErrorMessage(res)
765
+ err_string = (
766
+ string_at(err_char_p).decode("utf-8")
767
+ if err_char_p
768
+ else "Unknown error"
769
+ )
770
+ raise RuntimeError(
771
+ f"DDS Solver failed with code: {res} ({err_string})"
772
+ )
773
+
774
+ # 3. 結果を新しいウィンドウで表示
775
+ DDSResultsWindow(self.root, table_res, ["現在のハンド"], True)
776
+ self.status_var.set("ダブルダミー分析が完了しました。")
777
+
778
+ except Exception as e:
779
+ messagebox.showerror(
780
+ "DDS分析エラー", f"分析中にエラーが発生しました:\n{e}"
781
+ )
782
+ self.status_var.set("DDS分析中にエラーが発生しました。")
783
+
784
+ def open_debugger(self):
785
+ if not self.raw_rank_data:
786
+ messagebox.showwarning(
787
+ "データなし",
788
+ "デバッグするデータがありません。まず画像を分析してください。",
789
+ )
790
+ return
791
+ ColorDebuggerWindow(self.root, self.raw_rank_data)
792
+
793
+ def export_results(self):
794
+ if not self.all_result:
795
+ messagebox.showwarning(
796
+ "エクスポート不可", "エクスポートするデータがありません。"
797
+ )
798
+ return
799
+
800
+ save_path = filedialog.asksaveasfilename(
801
+ title="結果を保存",
802
+ defaultextension=".xhd",
803
+ filetypes=[
804
+ ("xhdファイル", "*.xhd"),
805
+ ("dupファイル", "*.dup"),
806
+ ("pbnファイル", "*.pbn"),
807
+ ],
808
+ initialfile=f"card_analysis_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
809
+ )
810
+
811
+ if not save_path:
812
+ return
813
+
814
+ filename, ext = os.path.splitext(os.path.basename(save_path))
815
+
816
+ encoding = "utf-8"
817
+ if ext == ".xhd":
818
+ text = convert2xhd(self.all_result, filename)
819
+ encoding = "shift_jis"
820
+ elif ext == ".dup":
821
+ text = convert2dup(self.all_result, filename)
822
+ elif ext == ".pbn":
823
+ text = convert2pbn(self.all_result, filename)
824
+ else:
825
+ text = convert2txt(self.all_result, filename)
826
+ print(text)
827
+ try:
828
+ with open(save_path, "w", encoding=encoding) as f:
829
+ f.write(text)
830
+
831
+ messagebox.showinfo(
832
+ "成功", f"結果が正常に保存されました:\n{save_path}"
833
+ )
834
+ self.status_var.set("結果をテキストファイルに出力しました。")
835
+ except Exception as e:
836
+ messagebox.showerror(
837
+ "エラー", f"ファイルへの書き込み中にエラーが発生しました:\n{e}"
838
+ )
839
+
840
+
841
+ class ColorDebuggerWindow(tk.Toplevel):
842
+ def __init__(self, parent, raw_data):
843
+ super().__init__(parent)
844
+ self.title("カラーデバッガー")
845
+ self.geometry("800x700")
846
+ self.raw_data = raw_data
847
+
848
+ # Entryウィジェット用の変数を初期化
849
+ self.l_thresh_var = tk.StringVar(
850
+ value=str(DEFAULT_THRESHOLDS["L_black"])
851
+ )
852
+ self.a_green_thresh_var = tk.StringVar(
853
+ value=str(DEFAULT_THRESHOLDS["a_green"])
854
+ )
855
+ self.a_black_thresh_var = tk.StringVar(
856
+ value=str(DEFAULT_THRESHOLDS["a_black"])
857
+ )
858
+ self.b_black_thresh_var = tk.StringVar(
859
+ value=str(DEFAULT_THRESHOLDS["b_black"])
860
+ )
861
+ self.a_red_thresh_var = tk.StringVar(
862
+ value=str(DEFAULT_THRESHOLDS["a_red"])
863
+ )
864
+ self.b_orange_thresh_var = tk.StringVar(
865
+ value=str(DEFAULT_THRESHOLDS["b_orange"])
866
+ )
867
+
868
+ # --- GUIレイアウト ---
869
+ # 制御フレーム
870
+ control_frame = ttk.Frame(self, padding=10)
871
+ control_frame.pack(fill=tk.X)
872
+
873
+ # 数値入力ボックス (Entry) を作成
874
+ ttk.Label(control_frame, text="L(黒)<").pack(side=tk.LEFT, padx=(0, 2))
875
+ ttk.Entry(control_frame, textvariable=self.l_thresh_var, width=8).pack(
876
+ side=tk.LEFT, padx=(0, 10)
877
+ )
878
+
879
+ ttk.Label(control_frame, text="a(緑)<").pack(side=tk.LEFT, padx=(0, 2))
880
+ ttk.Entry(
881
+ control_frame, textvariable=self.a_green_thresh_var, width=8
882
+ ).pack(side=tk.LEFT, padx=(0, 10))
883
+
884
+ ttk.Label(control_frame, text="a(黒)>").pack(side=tk.LEFT, padx=(0, 2))
885
+ ttk.Entry(
886
+ control_frame, textvariable=self.a_black_thresh_var, width=8
887
+ ).pack(side=tk.LEFT, padx=(0, 10))
888
+
889
+ ttk.Label(control_frame, text="b(黒)<").pack(side=tk.LEFT, padx=(0, 2))
890
+ ttk.Entry(
891
+ control_frame, textvariable=self.b_black_thresh_var, width=8
892
+ ).pack(side=tk.LEFT, padx=(0, 10))
893
+
894
+ ttk.Label(control_frame, text="a(赤)>").pack(side=tk.LEFT, padx=(0, 2))
895
+ ttk.Entry(
896
+ control_frame, textvariable=self.a_red_thresh_var, width=8
897
+ ).pack(side=tk.LEFT, padx=(0, 10))
898
+
899
+ ttk.Label(control_frame, text="b(橙)>").pack(side=tk.LEFT, padx=(0, 2))
900
+ ttk.Entry(
901
+ control_frame, textvariable=self.b_orange_thresh_var, width=8
902
+ ).pack(side=tk.LEFT, padx=(0, 10))
903
+
904
+ # 更新ボタン
905
+ ttk.Button(
906
+ control_frame,
907
+ text="閾値を更新して再判定",
908
+ command=self.update_classifications,
909
+ ).pack(side=tk.LEFT, padx=20)
910
+
911
+ # 結果表示用のキャンバスとスクロールバー
912
+ canvas_frame = ttk.Frame(self)
913
+ canvas_frame.pack(fill=tk.BOTH, expand=True)
914
+ self.canvas = tk.Canvas(canvas_frame)
915
+ scrollbar = ttk.Scrollbar(
916
+ canvas_frame, orient="vertical", command=self.canvas.yview
917
+ )
918
+ self.scrollable_frame = ttk.Frame(self.canvas)
919
+
920
+ self.scrollable_frame.bind(
921
+ "<Configure>",
922
+ lambda e: self.canvas.configure(
923
+ scrollregion=self.canvas.bbox("all")
924
+ ),
925
+ )
926
+ self.canvas.create_window(
927
+ (0, 0), window=self.scrollable_frame, anchor="nw"
928
+ )
929
+ self.canvas.configure(yscrollcommand=scrollbar.set)
930
+
931
+ self.canvas.pack(side="left", fill="both", expand=True)
932
+ scrollbar.pack(side="right", fill="y")
933
+
934
+ self.photo_images = []
935
+ self.update_classifications()
936
+
937
+ def update_classifications(self, event=None):
938
+ # 既存のウィジェットをクリア
939
+ for widget in self.scrollable_frame.winfo_children():
940
+ widget.destroy()
941
+
942
+ try:
943
+ # 入力された文字列を数値に変換
944
+ thresholds = DEFAULT_THRESHOLDS.copy()
945
+ thresholds["L_black"] = float(self.l_thresh_var.get())
946
+ thresholds["a_green"] = float(self.a_green_thresh_var.get())
947
+ thresholds["a_red"] = float(self.a_red_thresh_var.get())
948
+ thresholds["a_black"] = float(self.a_black_thresh_var.get())
949
+ thresholds["b_black"] = float(self.b_black_thresh_var.get())
950
+ thresholds["b_orange"] = float(self.b_orange_thresh_var.get())
951
+ except ValueError:
952
+ messagebox.showerror(
953
+ "入力エラー", "閾値には数値を入力してください。"
954
+ )
955
+ return
956
+
957
+ # 既存のウィジェットをクリア
958
+ for widget in self.scrollable_frame.winfo_children():
959
+ widget.destroy()
960
+ self.photo_images.clear() # 参照をクリア
961
+
962
+ for i, rank_data in enumerate(self.raw_data):
963
+ avg_lab = rank_data["avg_lab"]
964
+ color_name, avg_lab = get_suit_from_color_rules(
965
+ avg_lab, thresholds
966
+ )
967
+ rank_data["color"] = color_name
968
+
969
+ # --- 各ランクの情報を表示 ---
970
+ row_frame = ttk.Frame(self.scrollable_frame, padding=5)
971
+ row_frame.pack(fill=tk.X)
972
+
973
+ # 画像パッチを表示
974
+ pil_img = Image.fromarray(
975
+ cv2.cvtColor(rank_data["img"], cv2.COLOR_BGR2RGB)
976
+ )
977
+ pil_img.thumbnail((40, 50))
978
+ photo_img = ImageTk.PhotoImage(pil_img)
979
+ self.photo_images.append(photo_img)
980
+ ttk.Label(row_frame, image=photo_img).pack(side=tk.LEFT)
981
+
982
+ # 情報をテキストで表示
983
+ info_text = (
984
+ f"Player: {rank_data["player"]} | Rank: {rank_data['name']} | "
985
+ f"L:{avg_lab[0]:.1f} a:{avg_lab[1]:.1f} b:{avg_lab[2]:.1f} -> "
986
+ f"判定: {color_name.upper()}"
987
+ )
988
+ ttk.Label(
989
+ row_frame, text=info_text, font=("Courier New", 10)
990
+ ).pack(side=tk.LEFT, padx=10)
991
+
992
+
993
+ class DDSResultsWindow(tk.Toplevel):
994
+ def __init__(self, parent, solved_table, filenames, is_single):
995
+ super().__init__(parent)
996
+ self.title("ダブルダミー分析結果")
997
+ self.geometry("490x180")
998
+
999
+ notebook = ttk.Notebook(self)
1000
+ notebook.pack(pady=10, padx=10, fill="both", expand=True)
1001
+
1002
+ # print(solved_table.noOfBoards)
1003
+ if is_single:
1004
+ frame = ttk.Frame(self, padding="10")
1005
+ frame.pack(fill=tk.BOTH, expand=True)
1006
+
1007
+ # ★★★ 重要な修正点:columnsタプルに 'player' を明確に含める ★★★
1008
+ column_ids = ("player", "nt", "s", "h", "d", "c")
1009
+ tree = ttk.Treeview(frame, columns=column_ids, show="headings")
1010
+ tree.pack(fill=tk.BOTH, expand=True)
1011
+
1012
+ # ヘッダー(列のタイトル)の設定
1013
+ tree.heading("player", text="Declarer")
1014
+ tree.heading("nt", text="NT")
1015
+ tree.heading("s", text="Spades ♠")
1016
+ tree.heading("h", text="Hearts ♥")
1017
+ tree.heading("d", text="Diamonds ♦")
1018
+ tree.heading("c", text="Clubs ♣")
1019
+
1020
+ # 各列の幅と文字揃えを設定
1021
+ tree.column("player", width=80, anchor=tk.W, stretch=tk.NO)
1022
+ tree.column("nt", width=50, anchor=tk.CENTER)
1023
+ tree.column("s", width=80, anchor=tk.CENTER)
1024
+ tree.column("h", width=80, anchor=tk.CENTER)
1025
+ tree.column("d", width=80, anchor=tk.CENTER)
1026
+ tree.column("c", width=80, anchor=tk.CENTER)
1027
+
1028
+ # DDSライブラリの規約に合わせて、表示するプレイヤーの順番を定義
1029
+ players_map = {0: "North", 1: "East", 2: "South", 3: "West"}
1030
+ suits_map = {4: "nt", 0: "s", 1: "h", 2: "d", 3: "c"}
1031
+
1032
+ # テーブルデータを整形
1033
+ table_data = {p_name: {} for p_name in players_map.values()}
1034
+ for suit_idx, suit_name in suits_map.items():
1035
+ for player_idx, player_name in players_map.items():
1036
+ tricks = solved_table.resTable[suit_idx][player_idx]
1037
+ table_data[player_name][suit_name] = tricks
1038
+
1039
+ # テーブルにデータを挿入
1040
+ for player_name in ["North", "South", "East", "West"]:
1041
+ # valuesの最初の要素が 'player' カラムに対応
1042
+ row_values = [player_name] + [
1043
+ table_data[player_name][s]
1044
+ for s in ["nt", "s", "h", "d", "c"]
1045
+ ]
1046
+ tree.insert("", tk.END, values=tuple(row_values))
1047
+ else:
1048
+ for i in range(solved_table.noOfBoards // 20):
1049
+ filename = filenames[i]
1050
+ table = solved_table.results[i]
1051
+
1052
+ frame = ttk.Frame(notebook, padding="10")
1053
+ notebook.add(frame, text=os.path.basename(filename)[:20])
1054
+
1055
+ # Treeviewウィジェットでテーブルを作成
1056
+ tree = ttk.Treeview(
1057
+ frame,
1058
+ columns=("player", "nt", "s", "h", "d", "c"),
1059
+ show="headings",
1060
+ height=5,
1061
+ )
1062
+ tree.pack(fill=tk.BOTH, expand=True)
1063
+
1064
+ # ヘッダーの設定
1065
+ tree.heading("player", text="Declarer")
1066
+ tree.heading("nt", text="NT")
1067
+ tree.heading("s", text="Spades ♠")
1068
+ tree.heading("h", text="Hearts ♥")
1069
+ tree.heading("d", text="Diamonds ♦")
1070
+ tree.heading("c", text="Clubs ♣")
1071
+
1072
+ tree.column("player", width=80, anchor=tk.W, stretch=tk.NO)
1073
+ tree.column("nt", width=60, anchor=tk.CENTER)
1074
+ tree.column("s", width=80, anchor=tk.CENTER)
1075
+ tree.column("h", width=80, anchor=tk.CENTER)
1076
+ tree.column("d", width=80, anchor=tk.CENTER)
1077
+ tree.column("c", width=80, anchor=tk.CENTER)
1078
+
1079
+ table_data = reshape_table(table)
1080
+ print(table_data)
1081
+
1082
+ # テーブルにデータを挿入
1083
+ for player_name in ["North", "South", "East", "West"]:
1084
+ row_values = [player_name] + [
1085
+ table_data[player_name][s]
1086
+ for s in ["nt", "s", "h", "d", "c"]
1087
+ ]
1088
+ tree.insert("", tk.END, values=tuple(row_values))
1089
+
1090
+
1091
+ if __name__ == "__main__":
1092
+ root = tk.Tk()
1093
+ app = CardRecognizerApp(root)
1094
+ root.mainloop()
identify_cards.py ADDED
@@ -0,0 +1,808 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import math
3
+ import os
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image
8
+ from transformers import pipeline
9
+
10
+ from utils import arrange_hand
11
+
12
+ # # --- グローバル変数としてTrOCRパイプラインを初期化 ---
13
+ # print("TrOCRのAIモデルを読み込んでいます...(初回は数分かかります)")
14
+ # try:
15
+ # trocr_pipeline = pipeline(
16
+ # "image-to-text", model="microsoft/trocr-base-printed"
17
+ # )
18
+ # print("TrOCRの準備が完了しました。")
19
+ # except Exception as e:
20
+ # print(f"TrOCRモデルのロード中にエラーが発生しました: {e}")
21
+ # trocr_pipeline = None
22
+
23
+ generate_kwargs_sampling = {
24
+ "do_sample": True,
25
+ "temperature": 0.7,
26
+ "top_k": 50,
27
+ "max_length": 2,
28
+ }
29
+
30
+ SUIT_TEMPLATE_PATH = "templates/suits/"
31
+ SUITS_BY_COLOR = {"black": "S", "green": "C", "red": "H", "orange": "D"}
32
+ SCALE_STANDARD = 2032
33
+
34
+
35
+ def load_suit_templates(template_path):
36
+ templates = {}
37
+ if not os.path.exists(template_path):
38
+ return templates
39
+ for filename in os.listdir(template_path):
40
+ if filename.endswith(".png"):
41
+ name = os.path.splitext(filename)[0]
42
+ img = cv2.imread(
43
+ os.path.join(template_path, filename), cv2.IMREAD_GRAYSCALE
44
+ )
45
+ if img is not None:
46
+ templates[name] = img
47
+ return templates
48
+
49
+
50
+ def get_img_with_rect(img, rects, color, thickness):
51
+ _img = img.copy()
52
+ for rect in rects:
53
+ if isinstance(rect, tuple):
54
+ x, y, w, h = rect
55
+ else:
56
+ (x, y), (w, h) = rect["pos"], rect["size"]
57
+ cv2.rectangle(_img, (x, y), (x + w, y + h), color)
58
+
59
+ return _img
60
+
61
+
62
+ def save_img_with_rect(filename, img, rects, color=(0, 255, 0), thickness=2):
63
+ cv2.imwrite(filename, get_img_with_rect(img, rects, color, thickness))
64
+
65
+
66
+ def get_masks(hsv):
67
+ board_candidates = [
68
+ ((15, 200, 160), (35, 255, 245)), # yellow
69
+ ((100, 0, 0), (179, 60, 80)), # black
70
+ ((35, 200, 100), (50, 255, 160)), # light green
71
+ ((170, 170, 150), (179, 255, 220)), # red
72
+ ((160, 70, 170), (180, 120, 240)), # pink
73
+ ((0, 200, 160), (15, 255, 240)), # orange
74
+ ((90, 110, 160), (120, 210, 240)), # blue
75
+ ]
76
+ for color in board_candidates:
77
+ yield cv2.inRange(hsv, color[0], color[1])
78
+
79
+
80
+ def find_best_contour(image, is_best):
81
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
82
+ i = 0
83
+ for mask in get_masks(hsv):
84
+ kernel = np.ones((7, 7), np.uint8)
85
+ closed_mask = cv2.morphologyEx(
86
+ mask, cv2.MORPH_CLOSE, kernel, iterations=2
87
+ )
88
+
89
+ cv2.imwrite(f"debug_mask{i}.jpg", closed_mask)
90
+ contours, _ = cv2.findContours(
91
+ closed_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
92
+ )
93
+ best_contour = max(contours, key=cv2.contourArea) if contours else None
94
+ if best_contour is not None and cv2.contourArea(best_contour) > 50000:
95
+ b, res = is_best(best_contour)
96
+ if b:
97
+ return res
98
+ i += 1
99
+ return None
100
+
101
+
102
+ def find_board_corners(image):
103
+ """
104
+ 画像から黄色いボードの輪郭を見つけ、その4つの角の座標を返す。
105
+ """
106
+
107
+ def is_best(best_contour):
108
+ peri = cv2.arcLength(best_contour, True)
109
+ approx = cv2.approxPolyDP(best_contour, 0.02 * peri, True)
110
+
111
+ # 輪郭が4つの角を持つ場合、それを返す
112
+ is_rect = len(approx) >= 4
113
+ return is_rect, approx.reshape(-1, 2) if is_rect else None
114
+
115
+ points = find_best_contour(image, is_best)
116
+ # if not points:
117
+ # return None
118
+ sum = points.sum(axis=1)
119
+ diff = np.diff(points, axis=1)
120
+
121
+ top_left = points[np.argmin(sum)]
122
+ bottom_right = points[np.argmax(sum)]
123
+ top_right = points[np.argmax(diff)]
124
+ bottom_left = points[np.argmin(diff)]
125
+
126
+ corners = np.array(
127
+ [top_left, top_right, bottom_right, bottom_left], dtype="int32"
128
+ )
129
+
130
+ return corners
131
+
132
+
133
+ def order_points(pts):
134
+ """
135
+ 4つの点を左上、右上、右下、左下の順に並べ替える。
136
+ """
137
+ rect = np.zeros((4, 2), dtype="float32")
138
+ s = pts.sum(axis=1)
139
+ rect[0] = pts[np.argmin(s)] # 左上
140
+ rect[2] = pts[np.argmax(s)] # 右下
141
+
142
+ diff = np.diff(pts, axis=1)
143
+ rect[1] = pts[np.argmin(diff)] # 右上
144
+ rect[3] = pts[np.argmax(diff)] # 左下
145
+ return rect
146
+
147
+
148
+ def find_center_box(image):
149
+ def is_best(best_contour):
150
+ if best_contour is not None and cv2.contourArea(best_contour) > 50000:
151
+ print(f"rect:{cv2.boundingRect(best_contour)}")
152
+ x, y, w, h = cv2.boundingRect(best_contour)
153
+ h_parent, w_parent, _ = image.shape
154
+ if (w < h and (w > 0.34 * w_parent or h > 0.8 * h_parent)) or (
155
+ w >= h and (h > 0.34 * h_parent or w > 0.8 * w_parent)
156
+ ):
157
+ return False, None
158
+ return True, cv2.boundingRect(best_contour)
159
+ return False, None
160
+
161
+ return find_best_contour(image, is_best)
162
+
163
+
164
+ def find_center_seal(image, bw, bh):
165
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
166
+ mask = cv2.inRange(gray, 190, 255)
167
+ kernel = np.ones((7, 7), np.uint8)
168
+ closed_mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
169
+
170
+ cv2.imwrite(f"debug_seal.jpg", closed_mask)
171
+ contours, _ = cv2.findContours(
172
+ closed_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
173
+ )
174
+ for contour in contours:
175
+ x, y, w, h = cv2.boundingRect(contour)
176
+ _w = min(w, h)
177
+ _h = max(w, h)
178
+ if (
179
+ _w > 0.33 * bw
180
+ and _w < 0.44 * bw
181
+ and _h > 0.21 * bh
182
+ and _h < 0.24 * bh
183
+ ):
184
+ return x, y
185
+ return -1, -1
186
+
187
+
188
+ def rotate_rect(box, w_parent, h_parent, angle):
189
+ x, y, w, h = box
190
+ if angle % 360 == 0:
191
+ return (x, y, w, h)
192
+ elif angle % 360 == 90:
193
+ return (h_parent - h - y, x, h, w)
194
+ elif angle % 360 == 180:
195
+ return (w_parent - w - x, h_parent - h - y, w, h)
196
+ elif angle % 360 == 270:
197
+ return (y, w_parent - w - x, h, w)
198
+
199
+
200
+ def determine_and_correct_orientation(image, progress_fn):
201
+ """
202
+ 画像の向きを判断し、必要であれば回転させて補正した画像を返す。
203
+ """
204
+ progress_fn("写真の向きを自動分析中...")
205
+
206
+ # ★★★ ステップ1: ボードの4つの角を検出し、射影変換を行う ★★★
207
+ print("ボードの傾きを検出・補正しています...")
208
+ corners = find_board_corners(image)
209
+ print(corners)
210
+ if corners is None:
211
+ print("エラー: ボードの角を検出できませんでした。")
212
+ warped_image = image
213
+ else:
214
+ # 4つの角を正しい順序に並べ替える
215
+ ordered_corners = order_points(corners.astype(np.float32))
216
+ (tl, tr, br, bl) = ordered_corners
217
+
218
+ # 変換後の画像の幅と高さを計算
219
+ widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
220
+ widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
221
+ boardWidth = max(int(widthA), int(widthB))
222
+
223
+ heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
224
+ heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
225
+ boardHeight = max(int(heightA), int(heightB))
226
+
227
+ imageHeight, imageWidth, _ = image.shape
228
+ CanvasWidth, CanvasHeight = int(imageWidth * 1.2), int(
229
+ imageHeight * 1.2
230
+ )
231
+
232
+ x_offset = (CanvasWidth - boardWidth) // 2
233
+ y_offset = (CanvasHeight - boardHeight) // 2
234
+
235
+ # 変換後の座標を定義
236
+ dst_pts = np.array(
237
+ [
238
+ [x_offset, y_offset],
239
+ [x_offset + boardWidth - 1, y_offset],
240
+ [x_offset + boardWidth - 1, y_offset + boardHeight - 1],
241
+ [x_offset, y_offset + boardHeight - 1],
242
+ ],
243
+ dtype="float32",
244
+ )
245
+ print(dst_pts)
246
+
247
+ # 射影変換行列を取得し、画像を補正
248
+ matrix = cv2.getPerspectiveTransform(ordered_corners, dst_pts)
249
+ warped_image = cv2.warpPerspective(
250
+ image, matrix, (CanvasWidth, CanvasHeight)
251
+ )
252
+
253
+ # デバッグ用に補正後画像を保存
254
+ cv2.imwrite("debug_warped_image.jpg", warped_image)
255
+ print("傾き補正後の画像を debug_warped_image.jpg に保存しました。")
256
+
257
+ # まず中央のボードを見つける
258
+ box = find_center_box(warped_image)
259
+ if box is None:
260
+ progress_fn(
261
+ "警告: ボードが見つからないため、向きの自動補正をスキップします。"
262
+ )
263
+ return warped_image, box, 1
264
+ print("box is found")
265
+
266
+ bx, by, bw, bh = box
267
+ h, w, _ = warped_image.shape
268
+ scale = max(bw, bh) / SCALE_STANDARD
269
+ board_img = warped_image[by : by + bh, bx : bx + bw]
270
+ print(box)
271
+
272
+ image_rotated = warped_image.copy()
273
+ if bh < bw:
274
+ board_img = cv2.rotate(board_img, cv2.ROTATE_90_CLOCKWISE)
275
+ image_rotated = cv2.rotate(warped_image, cv2.ROTATE_90_CLOCKWISE)
276
+ box = rotate_rect(box, w, h, 90)
277
+ bx, by, bw, bh = box
278
+ h, w, _ = image_rotated.shape
279
+ _, sy = find_center_seal(board_img, bw, bh)
280
+ if sy == -1:
281
+ progress_fn("ボードのシールが検出できませんでした")
282
+ return image_rotated, box, scale
283
+ print(sy, bx, by, bw, bh)
284
+ if sy < bh / 2:
285
+ return image_rotated, box, scale
286
+ else:
287
+ return (
288
+ cv2.rotate(image_rotated, cv2.ROTATE_180),
289
+ rotate_rect(box, w, h, 180),
290
+ scale,
291
+ )
292
+
293
+
294
+ def get_not_white_mask(img):
295
+
296
+ lab_patch = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
297
+ l_channel = lab_patch[:, :, 0]
298
+ a_channel = lab_patch[:, :, 1]
299
+ b_channel = lab_patch[:, :, 2]
300
+ mask_l = cv2.threshold(l_channel, 170, 255, cv2.THRESH_BINARY_INV)[1]
301
+ mask_a = cv2.threshold(a_channel, 120, 255, cv2.THRESH_BINARY)[1]
302
+ mask_b = cv2.threshold(b_channel, 120, 255, cv2.THRESH_BINARY)[1]
303
+ mask_ab = cv2.bitwise_or(mask_a, mask_b)
304
+ text_mask = cv2.bitwise_and(mask_l, mask_ab)
305
+ return text_mask
306
+
307
+
308
+ # ★★★ 新しいルールベースの色判定関数(診断モード付き) ★★★
309
+ def get_suit_from_image_rules(rank_image_patch, thresholds):
310
+ if rank_image_patch is None or rank_image_patch.size == 0:
311
+ return "unknown"
312
+
313
+ text_mask = get_not_white_mask(rank_image_patch)
314
+ # デバッグ用のフォルダがなければ作成
315
+ debug_dir = "debug_chars"
316
+ if not os.path.exists(debug_dir):
317
+ os.makedirs(debug_dir)
318
+
319
+ # マスクを使って元のカラー画像から文字部分のみを抽出
320
+ masked_char_image = cv2.bitwise_and(
321
+ rank_image_patch, rank_image_patch, mask=text_mask
322
+ )
323
+
324
+ # ユニークなファイル名を生成
325
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
326
+ debug_filename = os.path.join(debug_dir, f"masked_char_{timestamp}.png")
327
+
328
+ # 画像を保存
329
+ cv2.imwrite(debug_filename, masked_char_image)
330
+
331
+ lab_patch = cv2.cvtColor(masked_char_image, cv2.COLOR_BGR2LAB)
332
+ avg_lab = cv2.mean(lab_patch, mask=text_mask)
333
+ if cv2.countNonZero(text_mask) < 20:
334
+ print(f" 診断: 文字ピクセルが少なすぎるため判定不可 {timestamp}")
335
+ return "unknown", avg_lab
336
+ return get_suit_from_color_rules(avg_lab, thresholds, timestamp)
337
+
338
+
339
+ def get_suit_from_color_rules(avg_lab, thresholds, timestamp=0):
340
+ L, a, b = avg_lab[0], avg_lab[1], avg_lab[2]
341
+
342
+ # --- 診断ログを出力 ---
343
+ print(f" L: {L:.1f}, a: {a:.1f}, b: {b:.1f} {timestamp}")
344
+
345
+ # ルールに基づいて判定
346
+ # ルール1: 明るさ(L)で黒を判定
347
+ if L < thresholds["L_black"]:
348
+ print(" ルール1: 明るさ(L)が低いため 'black' と判定")
349
+ return "black", avg_lab
350
+
351
+ # ルール2: a値で緑か赤系かを判断
352
+ # a < 128 が緑側, a > 128 が赤側
353
+ if a < thresholds["a_green"]: # 緑側の閾値
354
+ # if a > thresholds["a_black"] and b > thresholds["b_black"]:
355
+ # if a > thresholds["a_black"] and b < thresholds["b_black"]:
356
+ # print(" ルール6: 緑っぽいけど 'black' と判定")
357
+ # return "black", avg_lab
358
+ # elif a > thresholds["a_black2"] and b < thresholds["b_black2"]:
359
+ # print(" ルール8: 緑っぽいけど 'black' と判定2")
360
+ # return "black", avg_lab
361
+ if a + b > thresholds["ab_black"]:
362
+ print(" ルール9: a + bが高いため 'black' と判定")
363
+ return "black", avg_lab
364
+ if b - a < thresholds["ba_black"]:
365
+ print(" ルール6: b値がa値を下回っているため 'black' と判定")
366
+ return "black", avg_lab
367
+ print(" ルール : blackじゃないため 'green' と判定")
368
+ return "green", avg_lab
369
+ # if b > thresholds["b_black2"]:
370
+ # print(" ルール9: b値が高いため 'black' と判定")
371
+ # return "black", avg_lab
372
+ # if b - a > thresholds["ba_green"]:
373
+ # print(" ルール6: b値がa値を上回っているため 'green' と判定")
374
+ # return "green", avg_lab
375
+ # if b < thresholds["b_black"]:
376
+ # print(" ルール8: 緑っぽいけど 'black' と判定")
377
+ # return "black", avg_lab
378
+
379
+ # print(" ルール2: a値が低いため 'green' と判定")
380
+ # return "green", avg_lab
381
+ elif a > thresholds["a_red"]: # 赤側の閾値
382
+ # ルール3: b値で赤とオレンジを区別
383
+ # b > 128 が黄側, b < 128 が青側
384
+ if a - b > thresholds["a_b_red"]:
385
+ print(" ルール : a-bが大きいため 'red' と判定")
386
+ return "red", avg_lab
387
+ else:
388
+ print(" ルール : a-bが小さいため 'orange'と判定")
389
+ return "orange", avg_lab
390
+ # if b > thresholds["b_orange"]: # 黄色みが強ければオレンジ
391
+ # if b < thresholds["b_red"] and a > thresholds["a_orange"]:
392
+ # print(" ルール7: オレンジっぽいけど 'red' と判定")
393
+ # return "red", avg_lab
394
+ # print(" ルール3: a値が高く、b値も高いため 'orange' と判定")
395
+ # return "orange", avg_lab
396
+ # else: # それ以外は赤
397
+ # print(
398
+ # " ルール4: a値が高く、b値がそれほど高くないため 'red' と判定"
399
+ # )
400
+ # return "red", avg_lab
401
+
402
+ print(" ルール5: どのLABのルールにも一致しなかったため 'black' と判定")
403
+ return "black", avg_lab
404
+
405
+
406
+ def preprocess_img(img):
407
+ gray_region = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
408
+ debug_region = img.copy()
409
+
410
+ # --- ステップ1: カードマスクの作成 (変更なし) ---
411
+ _, card_mask = cv2.threshold(gray_region, 160, 255, cv2.THRESH_BINARY)
412
+ kernel_mask = np.ones((5, 5), np.uint8)
413
+ card_mask = cv2.dilate(card_mask, kernel_mask, iterations=3)
414
+ # cv2.imwrite(f"debug_card_mask_{player_name}.jpg", card_mask)
415
+
416
+ # --- ステップ2: Cannyエッジ検出による前処理 ---
417
+ # メディアンフィルタで元画像のノイズを軽く除去
418
+ denoised_gray = cv2.medianBlur(gray_region, 3)
419
+ thresholded = cv2.adaptiveThreshold(
420
+ denoised_gray,
421
+ 255,
422
+ cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
423
+ cv2.THRESH_BINARY_INV,
424
+ 21,
425
+ 7,
426
+ )
427
+ preprocessed = cv2.bitwise_and(thresholded, thresholded, mask=card_mask)
428
+ kernel_open = np.ones((3, 3), np.uint8)
429
+ preprocessed = cv2.morphologyEx(preprocessed, cv2.MORPH_OPEN, kernel_open)
430
+ return preprocessed
431
+
432
+
433
+ def filter_size(contours, scale, img):
434
+ res = []
435
+ for i, cnt in enumerate(contours):
436
+ x, y, w, h = cv2.boundingRect(cnt)
437
+ # サイズフィルタを適用
438
+ # if y < 400 and w * h > 1500 * scale * scale:
439
+ # print(f"{w}x{h} at ({x}, {y})")
440
+ if (
441
+ 40 * scale < h < 95 * scale
442
+ and 25 * scale < w < 60 * scale
443
+ and 0.25 < w / h < 0.9
444
+ and 1500 * scale * scale < w * h < 4000 * scale * scale
445
+ ):
446
+ pad = 10
447
+ cropped_img = img[
448
+ max(0, y - pad) : min(y + h + pad, img.shape[0]),
449
+ max(0, x - pad) : min(x + w + pad, img.shape[1]),
450
+ ]
451
+ no_padding_img = img[
452
+ max(0, y) : min(y + h, img.shape[0]),
453
+ max(0, x) : min(x + w, img.shape[1]),
454
+ ]
455
+
456
+ # hsv_patch = cv2.cvtColor(no_padding_img, cv2.COLOR_BGR2HSV)
457
+ # s_channel = hsv_patch[:, :, 1]
458
+ # v_channel = hsv_patch[:, :, 2]
459
+ # mask_s = cv2.threshold(s_channel, 30, 255, cv2.THRESH_BINARY)[1]
460
+ # mask_v = cv2.threshold(v_channel, 210, 255, cv2.THRESH_BINARY_INV)[
461
+ # 1
462
+ # ]
463
+ text_mask = get_not_white_mask(no_padding_img)
464
+ # デバッグ用のフォルダがなければ作成
465
+ debug_dir = "debug_chars"
466
+ if not os.path.exists(debug_dir):
467
+ os.makedirs(debug_dir)
468
+
469
+ # マスクを使って元のカラー画像から文字部分のみを抽出
470
+ masked_char_image = cv2.bitwise_and(
471
+ no_padding_img, no_padding_img, mask=text_mask
472
+ )
473
+ res.append(
474
+ {
475
+ "img": cropped_img,
476
+ "no_pad": no_padding_img,
477
+ "pos": (x, y),
478
+ "size": (w, h),
479
+ }
480
+ )
481
+ return res
482
+
483
+
484
+ def filter_thickness(
485
+ candidates,
486
+ scale,
487
+ ):
488
+ res = []
489
+ for candidate in candidates:
490
+ no_padding_img = candidate["no_pad"]
491
+ cropped_img = candidate["img"]
492
+
493
+ text_mask = get_not_white_mask(no_padding_img)
494
+ # デバッグ用のフォルダがなければ作成
495
+ debug_dir = "debug_chars"
496
+ if not os.path.exists(debug_dir):
497
+ os.makedirs(debug_dir)
498
+
499
+ # マスクを使って元のカラー画像から文字部分のみを抽出
500
+ masked_char_image = cv2.bitwise_and(
501
+ no_padding_img, no_padding_img, mask=text_mask
502
+ )
503
+ cropped_bin = cv2.cvtColor(masked_char_image, cv2.COLOR_BGR2GRAY)
504
+ cropped_dist = cv2.distanceTransform(cropped_bin, cv2.DIST_L2, 3)
505
+ _, max_val, _, _ = cv2.minMaxLoc(cropped_dist)
506
+
507
+ debug_dir = "debug_chars"
508
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
509
+ debug_filename = os.path.join(debug_dir, f"dist_char_{timestamp}.png")
510
+
511
+ # 画像を保存
512
+ cv2.imwrite(debug_filename, cropped_dist)
513
+
514
+ print(
515
+ f" 候補 at ({candidate['pos']}) - 厚みスコア: {max_val:.2f} {timestamp}"
516
+ )
517
+ if max_val > 12.0 * scale and max_val < 100000:
518
+ print(" -> スートと判断し除外")
519
+ continue
520
+ else:
521
+ print(" -> ランク候補として採用")
522
+
523
+ if cropped_img.size > 0:
524
+ candidate["thickness"] = max_val
525
+ res.append(candidate)
526
+ return res
527
+
528
+
529
+ def filter_suit(candidates, suit_templates, threshold):
530
+ filtered = []
531
+ for candidate in candidates:
532
+ is_suit = False
533
+
534
+ candidate_gray = cv2.cvtColor(candidate["img"], cv2.COLOR_BGR2GRAY)
535
+
536
+ for _, template in suit_templates.items():
537
+ resized_template = cv2.resize(
538
+ template, (candidate["size"][0], candidate["size"][1])
539
+ )
540
+ res = cv2.matchTemplate(
541
+ candidate_gray, resized_template, cv2.TM_CCOEFF_NORMED
542
+ )
543
+ _, max_val, _, _ = cv2.minMaxLoc(res)
544
+
545
+ if max_val > threshold:
546
+ is_suit = True
547
+ break
548
+ if not is_suit:
549
+ filtered.append(candidate)
550
+ return filtered
551
+
552
+
553
+ def filter_vertically(candidates, scale):
554
+ res = []
555
+ for candidate in candidates:
556
+ x, y = candidate["pos"]
557
+ is_eliminated = False
558
+ for _candidate in candidates:
559
+ _x, _y = _candidate["pos"]
560
+
561
+ if (
562
+ (
563
+ x - _x < 35 * scale
564
+ and x - _x > -35 * scale
565
+ and y - _y > 40 * scale
566
+ )
567
+ or (
568
+ x - _x < 80 * scale
569
+ and x - _x > -80 * scale
570
+ and y - _y > 90 * scale
571
+ )
572
+ or (
573
+ x - _x < 300 * scale
574
+ and x - _x > -300 * scale
575
+ and y - _y > 170 * scale
576
+ )
577
+ ):
578
+ is_eliminated = True
579
+ if not is_eliminated:
580
+ res.append(candidate)
581
+ return res
582
+
583
+
584
+ def filter_uniform(candidates, threshold=20.0):
585
+ res = []
586
+ for candidate in candidates:
587
+ text_mask = get_not_white_mask(candidate["no_pad"])
588
+ if cv2.countNonZero(text_mask) > 20:
589
+ lab_patch = cv2.cvtColor(candidate["no_pad"], cv2.COLOR_BGR2LAB)
590
+ _, std_dev = cv2.meanStdDev(lab_patch, mask=text_mask)
591
+
592
+ color_variance = math.sqrt(std_dev[1][0] ** 2 + std_dev[2][0] ** 2)
593
+
594
+ # デバッグ用に標準偏差を出力
595
+ print(
596
+ f" 候補 at ({candidate['pos']}) - 色のばらつき: {color_variance:.2f}"
597
+ )
598
+
599
+ # 標準偏差が閾値より小さければ、色が均一であると判断
600
+ if color_variance < threshold:
601
+ res.append(candidate)
602
+ else:
603
+ print(" -> 絵柄と判断し、除外")
604
+ return res
605
+
606
+
607
+ def find_rank_candidates(region_image, suit_templates, player_name, scale=1):
608
+ print(scale)
609
+ if region_image is None or region_image.size == 0:
610
+ return []
611
+
612
+ preprocessed = preprocess_img(region_image)
613
+ cv2.imwrite(f"debug_ocr_preprocess_{player_name}.jpg", preprocessed)
614
+
615
+ contours, _ = cv2.findContours(
616
+ preprocessed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
617
+ )
618
+
619
+ SUIT_FILTER_THRESHOLD = 0.6
620
+
621
+ candidates_size = filter_size(contours, scale, region_image)
622
+ candidates_thickness = filter_thickness(candidates_size, scale)
623
+ candidates_suit = filter_suit(
624
+ candidates_thickness, suit_templates, SUIT_FILTER_THRESHOLD
625
+ )
626
+ candidates_vertically = filter_vertically(candidates_suit, scale)
627
+ candidates_uniform = filter_uniform(candidates_vertically)
628
+
629
+ save_img_with_rect(
630
+ f"debug_ocr_thickness_{player_name}.jpg",
631
+ region_image,
632
+ candidates_thickness,
633
+ )
634
+ save_img_with_rect(
635
+ f"debug_ocr_candidates_{player_name}.jpg",
636
+ region_image,
637
+ candidates_size,
638
+ )
639
+ save_img_with_rect(
640
+ f"debug_filter_process_{player_name}.jpg",
641
+ region_image,
642
+ candidates_uniform,
643
+ )
644
+
645
+ print(
646
+ f"フィルタリング過程を debug_filter_process_{player_name}.jpg に保存しました。"
647
+ )
648
+ print(len(candidates_uniform))
649
+ return candidates_uniform
650
+
651
+
652
+ # ★★★ RGBで色を分析するヘルパー関数 ★★★
653
+ def get_avg_rgb_from_patch(patch):
654
+ """画像パッチから文字部分の平均色(RGB)を計算する"""
655
+ patch_gray = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
656
+ _, text_mask = cv2.threshold(patch_gray, 180, 230, cv2.THRESH_BINARY_INV)
657
+ if cv2.countNonZero(text_mask) == 0:
658
+ return None
659
+ # OpenCVの平均色はBGR順なので、RGB順に並べ替えて返す
660
+ avg_bgr = cv2.mean(patch, mask=text_mask)[:3]
661
+ return (avg_bgr[2], avg_bgr[1], avg_bgr[0]) # (R, G, B)
662
+
663
+
664
+ # ★★★ 最終改善版:Cannyエッジと輪郭階層を利用したカード認識関数 ★★★
665
+ def recognize_cards(region_image, suit_templates, player_name, trocr_pipeline):
666
+ rank_candidates = find_rank_candidates(
667
+ region_image, suit_templates, player_name
668
+ )
669
+ debug_region = region_image.copy()
670
+
671
+ VALID_RANKS = [
672
+ "A",
673
+ "K",
674
+ "Q",
675
+ "J",
676
+ "10",
677
+ "9",
678
+ "8",
679
+ "7",
680
+ "6",
681
+ "5",
682
+ "4",
683
+ "3",
684
+ "2",
685
+ ]
686
+
687
+ recognized_ranks = []
688
+ if rank_candidates and trocr_pipeline:
689
+ # 重複候補をマー��する
690
+ # ...(今回は省略。まずは検出できるかが重要)...
691
+
692
+ candidate_pil_images = [
693
+ Image.fromarray(cv2.cvtColor(c["img"], cv2.COLOR_BGR2RGB))
694
+ for c in rank_candidates
695
+ ]
696
+ ocr_results = trocr_pipeline(candidate_pil_images)
697
+ print([result[0]["generated_text"] for result in ocr_results])
698
+ # ocr_results = trocr_pipeline(candidate_pil_images, generate_kwargs=generate_kwargs_sampling)
699
+
700
+ for i, result in enumerate(ocr_results):
701
+ text = result[0]["generated_text"].upper().strip()
702
+ # TrOCRが誤認識しやすい文字を補正
703
+ if text == "1O":
704
+ text = "T"
705
+ if text == "0" or text == "O":
706
+ text = "T"
707
+
708
+ if text in VALID_RANKS:
709
+ candidate = rank_candidates[i]
710
+
711
+ # --- 診断ログを出力 ---
712
+ print(f"--- 診断中: ランク '{text}' at {candidate['pos']} ---")
713
+ color_name = get_suit_from_image_rules(candidate["img"])
714
+ print(f" -> 色判定結果: {color_name}")
715
+
716
+ if color_name in SUITS_BY_COLOR:
717
+ suit = SUITS_BY_COLOR[color_name]
718
+ card_name = f"{suit}{text}"
719
+ is_duplicate = any(
720
+ math.sqrt(
721
+ (fc["pos"][0] - candidate["pos"][0]) ** 2
722
+ + (fc["pos"][1] - candidate["pos"][1]) ** 2
723
+ )
724
+ < 20
725
+ for fc in recognized_ranks
726
+ )
727
+ if not is_duplicate:
728
+ recognized_ranks.append(
729
+ {"name": card_name, "pos": candidate["pos"]}
730
+ )
731
+
732
+ for card in recognized_ranks:
733
+ cv2.putText(
734
+ debug_region,
735
+ card["name"],
736
+ (card["pos"][0], card["pos"][1] - 10),
737
+ cv2.FONT_HERSHEY_SIMPLEX,
738
+ 1.0,
739
+ (255, 255, 0),
740
+ 2,
741
+ cv2.LINE_AA,
742
+ )
743
+ cv2.imwrite(f"debug_detection_{player_name}.jpg", debug_region)
744
+ print(
745
+ f"{player_name} の検出結果を debug_detection_{player_name}.jpg に保存しました。"
746
+ )
747
+ return [c["name"] for c in recognized_ranks]
748
+
749
+
750
+ def main(image_path):
751
+ suit_templates = load_suit_templates(SUIT_TEMPLATE_PATH)
752
+ if not suit_templates:
753
+ print(
754
+ "エラー: templates/suits フォルダにスートのテンプレート画像が見つかりません。"
755
+ )
756
+ return
757
+
758
+ image = cv2.imread(image_path)
759
+ if image is None:
760
+ return
761
+
762
+ box = find_center_box(image)
763
+ if box is None:
764
+ return
765
+ bx, by, bw, bh = box
766
+
767
+ h, w, _ = image.shape
768
+ margin = 200
769
+ player_regions = {
770
+ "north": image[0:by, :],
771
+ "south": image[by + bh : h, :],
772
+ "west": image[by - margin : by + bh + margin, 0:bx],
773
+ "east": image[by - margin : by + bh + margin, bx + bw : w],
774
+ }
775
+
776
+ for player, region in player_regions.items():
777
+ if region is None or region.size == 0:
778
+ continue
779
+ if player == "north":
780
+ player_regions[player] = cv2.rotate(region, cv2.ROTATE_180)
781
+ elif player == "east":
782
+ player_regions[player] = cv2.rotate(
783
+ region, cv2.ROTATE_90_CLOCKWISE
784
+ )
785
+ elif player == "west":
786
+ player_regions[player] = cv2.rotate(
787
+ region, cv2.ROTATE_90_COUNTERCLOCKWISE
788
+ )
789
+
790
+ all_hands = {}
791
+ for player, region in player_regions.items():
792
+ cards = recognize_cards(region, suit_templates, player)
793
+ all_hands[player] = arrange_hand(cards)
794
+
795
+ print("\n--- 最終識別結果 (ルールベース色判定) ---")
796
+ for player, hand in all_hands.items():
797
+ print(f"{player.capitalize()}: {', '.join(hand)}")
798
+ print("---------------------------------------")
799
+
800
+
801
+ if __name__ == "__main__":
802
+ IMAGE_FILE_PATH = "PXL_20250611_101254508.jpg"
803
+ # if trocr_pipeline:
804
+ # main(IMAGE_FILE_PATH)
805
+ # else:
806
+ # print("TrOCRパイプラインが初期化されていないため、処理を中止します。")
807
+ # else:
808
+ # print("TrOCRパイプラインが初期化されていないため、処理を中止します。")
libdds.a ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:500dca8660321df941d2242b35d29c26d3e2c1b38ffce61d18c00c37760957d7
3
+ size 910304
libdds.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6b4c3cb4beb9e41bf356b20e99d2efb59c3f6ac8606615f007a37d931d08bd4e
3
+ size 500488
main.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ import io
3
+ import os
4
+ from ctypes import c_int, pointer, string_at
5
+ from datetime import datetime
6
+
7
+ import cv2
8
+ import numpy as np
9
+ from fastapi import FastAPI, HTTPException, UploadFile
10
+ from fastapi.responses import JSONResponse
11
+ from PIL import Image
12
+
13
+ import dds
14
+ from app import (
15
+ DEFAULT_THRESHOLDS,
16
+ arrange_data,
17
+ format_dds_data,
18
+ get_player_regions,
19
+ validate_deal,
20
+ )
21
+ from identify_cards import (
22
+ SUIT_TEMPLATE_PATH,
23
+ determine_and_correct_orientation,
24
+ find_rank_candidates,
25
+ get_suit_from_image_rules,
26
+ load_suit_templates,
27
+ save_img_with_rect,
28
+ )
29
+ from utils import convert2pbn, convert2pbn_txt, is_text_valid
30
+
31
+ # from app import arrange_data, run_dds_analysis # Gradioのapp.pyからロジックを移植
32
+
33
+ # FastAPIインスタンスを作成
34
+ app = FastAPI()
35
+
36
+ # AIモデルとテンプレートを起動時に読み込む
37
+ trocr_pipeline = None # load_model()のロジックをここに
38
+ suit_templates = None
39
+
40
+
41
+ @app.on_event("startup")
42
+ def load_dependencies():
43
+ global trocr_pipeline, suit_templates
44
+ # TrOCRモデルをロード (Gradioのload_model関数を参考)
45
+ from transformers import pipeline
46
+
47
+ try:
48
+ print("Loading TrOCR model...")
49
+ trocr_pipeline = pipeline(
50
+ "image-to-text", model="microsoft/trocr-small-printed"
51
+ )
52
+ print("TrOCR model loaded.")
53
+ except Exception as e:
54
+ print(f"Failed to load TrOCR model: {e}")
55
+ trocr_pipeline = None
56
+
57
+ # スートテンプレートをロード
58
+ suit_templates = load_suit_templates("templates/suits/")
59
+
60
+
61
+ @app.post("/analyze/")
62
+ async def analyze_image(image_paths, progress):
63
+ global trocr_pipeline
64
+ # モデルが読み込まれているか確認
65
+ if trocr_pipeline is None:
66
+ print(
67
+ "AIモデルがまだ読み込まれていません。しばらく待ってから再度お試しください。"
68
+ )
69
+ # 空の更新を返すことで、UIの状態を変えずに処理を終了
70
+ return
71
+
72
+ all_results = []
73
+ num_total_files = len(image_paths)
74
+
75
+ progress(0, desc="テンプレート画像読み込み中...")
76
+ suit_templates = load_suit_templates(SUIT_TEMPLATE_PATH)
77
+ if not suit_templates:
78
+ raise (
79
+ f"エラー: {SUIT_TEMPLATE_PATH} フォルダにスートのテンプレート画像が見つかりません。"
80
+ )
81
+
82
+ try:
83
+ all_candidates_global = []
84
+ processed_files_info = []
85
+ # image_objects = {}
86
+
87
+ for i, image_path in enumerate(image_paths):
88
+ progress(
89
+ (i + 1) / num_total_files * 0.15,
90
+ desc="ステージ1/3: 文字候補を検出中...",
91
+ )
92
+ filename = os.path.basename(image_path)
93
+ progress(
94
+ (i + 1) / num_total_files * 0.3,
95
+ f"分析中 ({i+1}/{num_total_files}): {filename}",
96
+ )
97
+
98
+ try:
99
+ # ファイルをバイナリモードで安全に読み込む
100
+ with open(image_path, "rb") as f:
101
+ # バイトデータをNumPy配列に変換
102
+ file_bytes = np.asarray(
103
+ bytearray(f.read()), dtype=np.uint8
104
+ )
105
+ # NumPy配列(メモリ上のデータ)から画像をデコード
106
+ image = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
107
+
108
+ if image is None:
109
+ raise (
110
+ "OpenCVが画像をデコードできませんでした。ファイルが破損しているか、非対応の形式の可能性があります。"
111
+ )
112
+ # image_objects[filename] = image
113
+ except Exception as e:
114
+ # ファイル読み込み自体のエラーをキャッチ
115
+ all_results.append(
116
+ {"filename": filename, "error": f"画像読み込みエラー: {e}"}
117
+ )
118
+ # image_objects[filename] = None
119
+ continue
120
+
121
+ # box = find_center_box(image)
122
+ print("detect board")
123
+ rotated_image, box, scale = determine_and_correct_orientation(
124
+ image, lambda msg: print(msg)
125
+ )
126
+ if box is None:
127
+ all_results.append(
128
+ {"filename": filename, "error": "中央ボードの検出に失敗"}
129
+ )
130
+ continue
131
+ print(box)
132
+ save_img_with_rect("debug_rotated.jpg", rotated_image, [box])
133
+
134
+ MARGIN = 200
135
+ player_regions = get_player_regions(rotated_image, box, MARGIN)
136
+
137
+ for player, region in player_regions.items():
138
+ candidates = find_rank_candidates(
139
+ region, suit_templates, player, scale
140
+ )
141
+ for cand in candidates:
142
+ cand["filename"] = filename
143
+ cand["player"] = player
144
+ all_candidates_global.append(cand)
145
+
146
+ processed_files_info.append({"filename": filename, "error": None})
147
+ progress(
148
+ 0.4, desc="ステージ2/3: 文字認識を実行中... (時間がかかります)"
149
+ )
150
+
151
+ if not all_candidates_global or not trocr_pipeline:
152
+ progress(1, desc="認識する文字候補がありませんでした。")
153
+ print("認識する文字候補がありませんでした。")
154
+ return all_results # エラーがあった画像の結果だけを返す
155
+
156
+ try:
157
+ candidates_pil_images = [
158
+ Image.fromarray(cv2.cvtColor(c["img"], cv2.COLOR_BGR2RGB))
159
+ for c in all_candidates_global
160
+ ]
161
+ ocr_results = trocr_pipeline(candidates_pil_images)
162
+ except Exception as e:
163
+ print(f"OCR処理中にエラーが発生しました: {e}")
164
+
165
+ # --- ステージ3: 結果の仕分けと最終的なカードの特定 ---
166
+ progress(0.9, desc="ステージ3/3: 認識結果を仕分け中...")
167
+
168
+ print([result[0]["generated_text"] for result in ocr_results])
169
+
170
+ raw_data = []
171
+ # blacks = []
172
+ # reds = []
173
+ for i, result in enumerate(ocr_results):
174
+ text = result[0]["generated_text"].upper().strip()
175
+ print(text, is_text_valid(text))
176
+
177
+ text = is_text_valid(text)
178
+ if text is not None:
179
+ candidate_info = all_candidates_global[i]
180
+ print(
181
+ f"--- 診断中: ランク '{text}' of {candidate_info['player']} at {candidate_info['pos']} with thick:{candidate_info['thickness']} ---"
182
+ )
183
+ color_name, avg_lab = get_suit_from_image_rules(
184
+ candidate_info["no_pad"], DEFAULT_THRESHOLDS
185
+ )
186
+ print(color_name)
187
+ if color_name == "mark":
188
+ continue
189
+ candidate_info["avg_lab"] = avg_lab
190
+ candidate_info["color"] = color_name
191
+ candidate_info["name"] = text
192
+ raw_data.append(candidate_info)
193
+
194
+ # print("\r\n".join(blacks))
195
+ # print("\r\n".join(reds))
196
+
197
+ all_results = arrange_data(raw_data)
198
+ pbn_content = convert2pbn(all_results)
199
+ pbn_filename = f"analysis_{datetime.now().strftime('%Y%m%d')}.pbn"
200
+ # if processed_files_info:
201
+ # last_result = {"filename": processed_files_info[0]["filename"], 1ands": all_results[0][1ands"]}
202
+
203
+ # if all_results:
204
+ # # ダウンロード用にPBNコンテンツを値として設定し、表示状態にする
205
+ # export_update = gr.update(interactive=True)
206
+ # else:
207
+ # export_update = gr.update(interactive=False)
208
+ final_result = all_results[0]["hands"]
209
+ filenames = [os.path.basename(p) for p in image_paths]
210
+ # dropdown_update = gr.update(
211
+ # choices=filenames, value=filenames[0], interactive=True, open=True
212
+ # )
213
+
214
+ dataframes = run_dds_analysis(all_results, progress)
215
+ for result in all_results:
216
+ if result["filename"] in dataframes.keys():
217
+ result["dds"] = dataframes[result["filename"]]
218
+
219
+ return JSONResponse(content=all_results)
220
+
221
+ except Exception as e:
222
+ raise (f"致命的なエラー: {e}")
223
+
224
+
225
+ def run_dds_analysis(all_results_state):
226
+ """ダブルダミー分析を実行する"""
227
+ valid_deals = []
228
+ for result in all_results_state:
229
+ if "hands" in result:
230
+ is_valid, _ = validate_deal(result["hands"])
231
+ if is_valid:
232
+ valid_deals.append(result)
233
+
234
+ if len(valid_deals) == 0:
235
+ raise ("分析不可", "分析対象となる正常なディールがありません。")
236
+
237
+ # self.status_var.set(f"{len(valid_deals)}件のディールを分析中...")
238
+
239
+ try:
240
+ deals = dds.ddTableDealsPBN()
241
+ deals.noOfTables = len(valid_deals)
242
+ for i, result in enumerate(valid_deals):
243
+ pbn_deal_string = convert2pbn_txt(result["hands"], "N")
244
+ print(pbn_deal_string)
245
+
246
+ # table_deal_pbn = dds.ddTableDealPBN()
247
+ # table_deal_pbn.cards = pbn_deal_string.encode("utf-8")
248
+
249
+ deals.deals[i].cards = pbn_deal_string.encode("utf-8")
250
+
251
+ dds.SetMaxThreads(0)
252
+ table_res = dds.ddTablesRes()
253
+ per_res = dds.allParResults()
254
+ # table_res_pointer = pointer(table_res)
255
+ res = dds.CalcAllTablesPBN(
256
+ pointer(deals),
257
+ 0,
258
+ (c_int * 5)(0, 0, 0, 0, 0),
259
+ pointer(table_res),
260
+ pointer(per_res),
261
+ )
262
+ print("dds")
263
+
264
+ if res != dds.RETURN_NO_FAULT:
265
+ err_char_p = dds.ErrorMessage(res)
266
+ err_string = (
267
+ string_at(err_char_p).decode("utf-8")
268
+ if err_char_p
269
+ else "Unknown error"
270
+ )
271
+ raise RuntimeError(
272
+ f"DDS Solver failed with code: {res} ({err_string})"
273
+ )
274
+ print("dds")
275
+
276
+ filenames = [d["filename"] for d in valid_deals]
277
+ dataframes = {}
278
+ for i, filename in enumerate(filenames):
279
+ headers, rows = format_dds_data(table_res.results[i].resTable)
280
+ print(rows)
281
+ dataframes[filename] = rows
282
+
283
+ return dataframes
284
+
285
+ # 3. 結果を新しいウィンドウで表示
286
+
287
+ except Exception as e:
288
+ raise (f"DDS分析エラー: 分析中にエラーが発生しました:\n{e}")
289
+ # self.status_var.set("DDS分析中にエラーが発生しました。")
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ libboost-thread-dev
requirements.txt ADDED
Binary file (4.64 kB). View file
 
templates/ranks/10.png ADDED
templates/ranks/2.png ADDED
templates/ranks/3.png ADDED
templates/ranks/4.png ADDED
templates/ranks/5.png ADDED
templates/ranks/6.png ADDED
templates/ranks/7.png ADDED
templates/ranks/8.png ADDED
templates/ranks/9.png ADDED
templates/ranks/A.png ADDED
templates/ranks/J.png ADDED
templates/ranks/K.png ADDED
templates/ranks/Q.png ADDED
templates/suits/club.png ADDED
templates/suits/diamond.png ADDED
templates/suits/heart.png ADDED
templates/suits/spade.png ADDED
utils.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ctypes import c_long, c_uint, c_ulong
2
+
3
+ import dds
4
+
5
+ SUIT_ORDER = {"S": 0, "H": 1, "D": 2, "C": 3}
6
+ RANK_ORDER = {
7
+ "A": 14,
8
+ "K": 13,
9
+ "Q": 12,
10
+ "J": 11,
11
+ "T": 10,
12
+ "9": 9,
13
+ "8": 8,
14
+ "7": 7,
15
+ "6": 6,
16
+ "5": 5,
17
+ "4": 4,
18
+ "3": 3,
19
+ "2": 2,
20
+ }
21
+
22
+
23
+ RANK_STRENGTH = {
24
+ "A": 1,
25
+ "K": 2,
26
+ "Q": 3,
27
+ "J": 4,
28
+ "T": 5,
29
+ "9": 6,
30
+ "8": 7,
31
+ "7": 8,
32
+ "6": 9,
33
+ "5": 10,
34
+ "4": 11,
35
+ "3": 12,
36
+ "2": 13,
37
+ }
38
+ SUIT_STRENGTH = {
39
+ "S": 0,
40
+ "H": 1,
41
+ "D": 2,
42
+ "C": 3,
43
+ }
44
+ RANK_TO_BIT = {rank: 2**power for rank, power in RANK_ORDER.items()}
45
+
46
+
47
+ def is_text_valid(text):
48
+ text = (
49
+ text.replace("1O", "T")
50
+ .replace("10", "T")
51
+ .replace("0", "T")
52
+ .replace("O", "T")
53
+ .replace("U", "J")
54
+ .replace("1", "7")
55
+ )
56
+ for rank in RANK_ORDER.keys():
57
+ if rank in text:
58
+ return rank
59
+ return None
60
+
61
+
62
+ def arrange_hand(hand):
63
+ unique_hand = list(set(hand))
64
+
65
+ sorted_hand = sorted(
66
+ unique_hand,
67
+ key=lambda card: (
68
+ SUIT_ORDER.get(card[0], 99),
69
+ -RANK_ORDER.get(card[1:], 0),
70
+ ),
71
+ )
72
+ return sorted_hand
73
+
74
+
75
+ def convert2xhd(all_result, title):
76
+ xhd = f"""<?xml version="1.0" encoding="shift_jis"?>
77
+ <HandData>
78
+ <Prop>
79
+ <Title>{title}</Title>
80
+ </Prop>
81
+ """
82
+ for i, result in enumerate(all_result):
83
+ if "error" in result.keys():
84
+ continue
85
+ xhd += f'<Board id="{i+1}">\r\n'
86
+ xhd += convert2xhd_board(result["hands"])
87
+ xhd += "\r\n</Board>\r\n"
88
+ xhd += "</HandData>"
89
+ return xhd
90
+
91
+
92
+ def convert2xhd_board(board):
93
+ north_hand = convert2xhd_hand(board["north"])
94
+ south_hand = convert2xhd_hand(board["south"])
95
+ west_hand = convert2xhd_hand(board["west"])
96
+ east_hand = convert2xhd_hand(board["east"])
97
+
98
+ return f"<Deal>{north_hand} {east_hand} {south_hand} {west_hand}</Deal>"
99
+
100
+
101
+ def convert2xhd_hand(hand):
102
+ arranged_cards = arrange_hand(hand)
103
+ S, H, D, C = "", "", "", ""
104
+
105
+ for card in arranged_cards:
106
+ if card[0] == "S":
107
+ S += card[1:]
108
+ if card[0] == "H":
109
+ H += card[1:]
110
+ if card[0] == "D":
111
+ D += card[1:]
112
+ if card[0] == "C":
113
+ C += card[1:]
114
+
115
+ return f"{S}.{H}.{D}.{C}"
116
+
117
+
118
+ def convert2dup(all_result, _):
119
+ num = len(all_result)
120
+ dup = ""
121
+ for result in all_result:
122
+ if "error" in result.keys():
123
+ continue
124
+ dup += convert2dup_board(result["hands"], num)
125
+ return dup
126
+
127
+
128
+ def convert2dup_board(board, num):
129
+ north_hand_num = convert2dup_hand_num(board["north"])
130
+ east_hand_num = convert2dup_hand_num(board["east"])
131
+ south_hand_num = convert2dup_hand_num(board["south"])
132
+ north_hand = convert2dup_hand_str(board["north"])
133
+ south_hand = convert2dup_hand_str(board["south"])
134
+ west_hand = convert2dup_hand_str(board["west"])
135
+ east_hand = convert2dup_hand_str(board["east"])
136
+
137
+ return f"{north_hand_num}{east_hand_num}{south_hand_num}{north_hand}{east_hand}{south_hand}{west_hand}YN1 1 {str(num).ljust(2)} "
138
+
139
+
140
+ def convert2dup_hand_num(hand):
141
+ arranged_hand = arrange_hand(hand)
142
+
143
+ cards_num = [convert_card2num(card) for card in arranged_hand]
144
+ sorted(cards_num)
145
+
146
+ return "".join(str(c).zfill(2) for c in cards_num)
147
+
148
+
149
+ def convert2dup_hand_str(hand):
150
+ arranged_hand = arrange_hand(hand)
151
+ S, H, D, C = "", "", "", ""
152
+
153
+ for card in arranged_hand:
154
+ if card[0] == "S":
155
+ S += card[1]
156
+ if card[0] == "H":
157
+ H += card[1]
158
+ if card[0] == "D":
159
+ D += card[1]
160
+ if card[0] == "C":
161
+ C += card[1]
162
+
163
+ return f"{S}{H}{D}{C}"
164
+
165
+
166
+ def convert_card2num(card):
167
+ suit = SUIT_STRENGTH.get(card[0])
168
+ rank = RANK_STRENGTH.get(card[1])
169
+ if suit is None or rank is None:
170
+ return -1
171
+ return suit * 13 + rank
172
+
173
+
174
+ def convert2pbn(all_result):
175
+ res = "% Dealer4 ver 4.82\r\n"
176
+
177
+ for i, result in enumerate(all_result):
178
+ if "error" in result.keys():
179
+ continue
180
+ res += '[Event "#"]\r\n' if i != 0 else '[Event ""]\r\n'
181
+ res += '[Site "#"]\r\n' if i != 0 else '[Site ""]\r\n'
182
+ res += '[Date "#"]\r\n' if i != 0 else '[Date ""]\r\n'
183
+ res += f'[Board "{i}"]\r\n'
184
+ res += f'[Dealer "{get_dealer(i)}"]\r\n'
185
+ res += f'[Vulnerable "{get_vul(i)}"]\r\n'
186
+ res += f'{convert2pbn_board(result["hands"], get_dealer(i))}\r\n\r\n'
187
+
188
+ return res
189
+
190
+
191
+ def convert2pbn_board(board, dealer):
192
+ return f'[Deal "{convert2pbn_txt(board, dealer)}"]'
193
+
194
+
195
+ def convert2pbn_txt(board, dealer):
196
+ player_order = ["north", "south", "west", "east"]
197
+ dealer_dict = {"N": 0, "E": 1, "S": 2, "W": 3}
198
+ north_hand = convert2pbn_hand(board[player_order[dealer_dict[dealer]]])
199
+ south_hand = convert2pbn_hand(
200
+ board[player_order[(dealer_dict[dealer] + 1) % 4]]
201
+ )
202
+ west_hand = convert2pbn_hand(
203
+ board[player_order[(dealer_dict[dealer] + 2) % 4]]
204
+ )
205
+ east_hand = convert2pbn_hand(
206
+ board[player_order[(dealer_dict[dealer] + 3) % 4]]
207
+ )
208
+
209
+ return f"{dealer}:{north_hand} {east_hand} {south_hand} {west_hand}"
210
+
211
+
212
+ def convert2pbn_hand(hand):
213
+ arranged_cards = arrange_hand(hand)
214
+ S, H, D, C = "", "", "", ""
215
+
216
+ for card in arranged_cards:
217
+ if card[0] == "S":
218
+ S += card[1:]
219
+ if card[0] == "H":
220
+ H += card[1:]
221
+ if card[0] == "D":
222
+ D += card[1:]
223
+ if card[0] == "C":
224
+ C += card[1:]
225
+
226
+ return f"{S}.{H}.{D}.{C}"
227
+
228
+
229
+ def get_vul(num):
230
+ r = num % 4
231
+ q = (num - 1) // 4
232
+
233
+ if (r + q) % 4 == 1:
234
+ return "None"
235
+ elif (r + q) % 4 == 2:
236
+ return "NS"
237
+ elif (r + q) % 4 == 3:
238
+ return "EW"
239
+ else:
240
+ return "All"
241
+
242
+
243
+ def get_dealer(num):
244
+ if num % 4 == 1:
245
+ return "N"
246
+ elif num % 4 == 2:
247
+ return "E"
248
+ elif num % 4 == 3:
249
+ return "S"
250
+ else:
251
+ return "W"
252
+
253
+
254
+ def convert2ddTableDeal(hands_dict):
255
+ table_deal = dds.ddTableDeal()
256
+
257
+ # C言語の配列のように振る舞うため、このように初期化
258
+ cards = ((c_uint * 4) * 4)() # ddTableDealはc_uintでOK
259
+
260
+ hand_map = {"north": 0, "east": 1, "south": 2, "west": 3}
261
+ suit_map = {"S": 0, "H": 1, "D": 2, "C": 3}
262
+
263
+ for player_name, hand in hands_dict.items():
264
+ for card in hand:
265
+ suit_char = card[0]
266
+ rank_char = card[1:]
267
+
268
+ player_idx = hand_map.get(player_name)
269
+ suit_idx = suit_map.get(suit_char)
270
+
271
+ if player_idx is not None and suit_idx is not None:
272
+ cards[player_idx][suit_idx] |= RANK_TO_BIT.get(rank_char, 0)
273
+
274
+ table_deal.cards = cards
275
+ return table_deal
276
+
277
+
278
+ # DDSのdealオブジェクトを作成する最終確定版の関数
279
+ def convert_hands_to_binary_deal(hands_dict):
280
+ from ctypes import c_uint
281
+
282
+ deal = dds.deal()
283
+ deal.trump = 0
284
+ deal.first = 0
285
+ deal.currentTrickSuit = (0, 0, 0)
286
+ deal.currentTrickRank = (0, 0, 0)
287
+
288
+ cards = ((c_long * 4) * 4)()
289
+ hand_map = {"north": 0, "east": 1, "south": 2, "west": 3}
290
+ suit_map = {"S": 0, "H": 1, "D": 2, "C": 3}
291
+
292
+ for player_name, hand in hands_dict.items():
293
+ for card in hand:
294
+ suit_char = card[0]
295
+ rank_char = card[1:]
296
+ player_idx = hand_map.get(player_name)
297
+ suit_idx = suit_map.get(suit_char)
298
+ if player_idx is not None and suit_idx is not None:
299
+ cards[player_idx][suit_idx] |= RANK_TO_BIT.get(rank_char, 0)
300
+
301
+ deal.remainCards = cards
302
+ return deal
303
+
304
+
305
+ def PrintTable(table):
306
+ dcardSuit = ["S", "H", "D", "C", "N"]
307
+ print(
308
+ "{:5} {:<5} {:<5} {:<5} {:<5}".format(
309
+ "", "North", "South", "East", "West"
310
+ )
311
+ )
312
+ print(
313
+ "{:>5} {:5} {:5} {:5} {:5}".format(
314
+ "NT",
315
+ table.resTable[0][4],
316
+ table.resTable[2][4],
317
+ table.resTable[1][4],
318
+ table.resTable[3][4],
319
+ )
320
+ )
321
+ for suit in range(0, dds.DDS_SUITS):
322
+ print(
323
+ "{:>5} {:5} {:5} {:5} {:5}".format(
324
+ dcardSuit[suit],
325
+ table.resTable[0][suit],
326
+ table.resTable[2][suit],
327
+ table.resTable[1][suit],
328
+ table.resTable[3][suit],
329
+ )
330
+ )
331
+ print("")
332
+
333
+
334
+ def reshape_table(table):
335
+ players_map = {0: "North", 1: "East", 2: "South", 3: "West"}
336
+ suits_map = {4: "nt", 0: "s", 1: "h", 2: "d", 3: "c"}
337
+ res = {p_name: {} for p_name in players_map.values()}
338
+
339
+ for i, suit in suits_map.items():
340
+ for j, player in players_map.items():
341
+ # new_suit = suits_map[(i + 5 * j) // 4]
342
+ # new_player = players_map[(i + 5 * j) % 4]
343
+ # print(
344
+ # suit, player, i, j, new_suit, new_player, table.resTable[j][i]
345
+ # )
346
+ res[player][suit] = table.resTable[i][j]
347
+ return res