vincentlui commited on
Commit
6ee449a
1 Parent(s): 6639f73

hand record

Browse files
Files changed (4) hide show
  1. app.py +80 -39
  2. hand_record.py +99 -0
  3. pbn_util.py +137 -0
  4. requirements.txt +2 -1
app.py CHANGED
@@ -4,21 +4,21 @@ import subprocess
4
  import os
5
  from tempfile import mkdtemp
6
  from timeit import default_timer as timer
 
 
7
 
8
  # Download model and libraries from repo
9
- try:
10
- token = os.environ.get("model_token")
11
- subprocess.run(["git", "clone", f"https://oauth2:{token}@huggingface.co/vincentlui/bridge_hand_detect"])
12
- except:
13
- print('Fail to download code')
14
 
15
  try:
16
  from bridge_hand_detect2.predict import CardDetectionModel
17
- from bridge_hand_detect2.pbn import create_pbn_file
18
  except Exception as e:
19
  print(e)
20
  from bridge_hand_detect.predict import CardDetectionModel
21
- from bridge_hand_detect.pbn import create_pbn_file
22
 
23
  custom_css = \
24
  """
@@ -33,18 +33,19 @@ OUTPUT_IMG_HEIGHT = 320
33
  css = ".output_img {display:block; margin-left: auto; margin-right: auto}"
34
  model = CardDetectionModel()
35
 
36
- def predict(image_path):
 
37
  start = timer()
38
  df = None
39
  try:
40
- hands, (width,height) = model(image_path, augment=True)
41
  print(hands)
42
  # Output dataframe
43
- df = pd.DataFrame(['♠', '♥', '♦', '♣'], columns=[''])
44
  for hand in hands:
45
  df[hand.direction] = [''.join(c) for c in hand.cards]
46
  except:
47
- gr.Error('Cannot process image')
48
 
49
  end = timer()
50
  print(f'Process time: {end - start:.02f} seconds')
@@ -52,25 +53,38 @@ def predict(image_path):
52
  return df
53
 
54
 
55
- default_df = df = pd.DataFrame({'':['♠', '♥', '♦', '♣'],
56
  'N': ['']*4,
57
  'E': ['']*4,
58
  'S': ['']*4,
59
  'W': ['']*4})
60
 
61
 
62
- def save_file2(df, cache_dir, total):
63
  d = cache_dir
64
  if cache_dir is None:
65
  d = mkdtemp()
66
- total += 1
67
- file_name = f'bridgehand{total:03d}.pbn'
 
 
 
 
 
 
 
 
68
  file_path = os.path.join(d,file_name)
69
  with open(file_path, 'w') as f:
70
- pbn_str = create_pbn_file(df)
71
  f.write(pbn_str)
72
 
73
- return file_path, d, total
 
 
 
 
 
 
74
 
75
  with gr.Blocks(css=custom_css) as demo:
76
  gr.Markdown(
@@ -81,35 +95,62 @@ with gr.Blocks(css=custom_css) as demo:
81
  The results can be exported as a PBN file, which can be imported to other bridge software such as double dummy solvers.
82
  1. Upload an image showing all four hands fanned as shown in the example.
83
  2. Click *Submit*. The scan result will be displayed in the table.
84
- 3. Verify the output and correct any missing or wrong card in the table. Then click *Save* to generate a PBN file.
 
 
 
85
 
86
  Tips:
87
- - This AI reads the values at the top left corner of the playing cards. Make sure they are visible and as large as possible.
88
  - To get the best accuracy, place the cards following the layout in the examples.
89
 
90
- I need your feedback to make this app more useful. Please send your comments to <vincentlui123@gmail.com>.
91
  """)
92
 
93
  total = gr.State(0)
94
  gradio_cache_dir = gr.State()
95
-
96
- with gr.Row():
97
- with gr.Column():
98
- a1 = gr.Image(type="filepath",sources=['upload'],interactive=True,height=INPUT_IMG_HEIGHT)
99
- with gr.Row():
100
- a2 = gr.ClearButton()
101
- a3 = gr.Button('Submit',variant="primary")
102
- a4 = gr.Examples('examples', a1)
103
-
104
- with gr.Column():
105
- b1 = gr.Dataframe(value=default_df, datatype="str", row_count=(4,'fixed'), col_count=(5,'fixed'),
106
- headers=['', 'N', 'E', 'S', 'W'],
107
- interactive=True, column_widths=['8%', '23%','23%','23%','23%'])
108
- b2 = gr.Button('Save')
109
- b3 = gr.File(interactive=False)
110
-
111
- a2.add([a1, b1, b3])
112
- a3.click(predict, [a1], [b1])
113
- b2.click(save_file2, [b1, gradio_cache_dir, total], [b3, gradio_cache_dir, total])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  demo.queue().launch()
 
4
  import os
5
  from tempfile import mkdtemp
6
  from timeit import default_timer as timer
7
+ from hand_record import create_hand_record_pdf
8
+ from pbn_util import validate_pbn, create_single_pbn_string
9
 
10
  # Download model and libraries from repo
11
+ # try:
12
+ # token = os.environ.get("model_token")
13
+ # subprocess.run(["git", "clone", f"https://oauth2:{token}@huggingface.co/vincentlui/bridge_hand_detect"])
14
+ # except:
15
+ # print('Fail to download code')
16
 
17
  try:
18
  from bridge_hand_detect2.predict import CardDetectionModel
 
19
  except Exception as e:
20
  print(e)
21
  from bridge_hand_detect.predict import CardDetectionModel
 
22
 
23
  custom_css = \
24
  """
 
33
  css = ".output_img {display:block; margin-left: auto; margin-right: auto}"
34
  model = CardDetectionModel()
35
 
36
+ def predict(image_path, top_hand_idx):
37
+ print(top_hand_idx)
38
  start = timer()
39
  df = None
40
  try:
41
+ hands, (width,height) = model(image_path, augment=True, top_hand_idx=top_hand_idx)
42
  print(hands)
43
  # Output dataframe
44
+ df = default_df.copy(deep=True)#pd.DataFrame(['♠', '♥', '♦', '♣'], columns=[''])
45
  for hand in hands:
46
  df[hand.direction] = [''.join(c) for c in hand.cards]
47
  except:
48
+ raise gr.Error('Cannot process image')
49
 
50
  end = timer()
51
  print(f'Process time: {end - start:.02f} seconds')
 
53
  return df
54
 
55
 
56
+ default_df = pd.DataFrame({'':['♠', '♥', '♦', '♣'],
57
  'N': ['']*4,
58
  'E': ['']*4,
59
  'S': ['']*4,
60
  'W': ['']*4})
61
 
62
 
63
+ def save_file(df, cache_dir, files, board_no):
64
  d = cache_dir
65
  if cache_dir is None:
66
  d = mkdtemp()
67
+
68
+ pbn_str = create_single_pbn_string(df, board_no=board_no)
69
+ try:
70
+ validate_pbn(pbn_str)
71
+ except Exception as e:
72
+ print(e)
73
+ gr.Warning(f'Fail to save: {e}')
74
+ return files, files, d
75
+
76
+ file_name = f'board_{board_no:03d}.pbn'
77
  file_path = os.path.join(d,file_name)
78
  with open(file_path, 'w') as f:
 
79
  f.write(pbn_str)
80
 
81
+ if not file_path in files:
82
+ files.append(file_path)
83
+ return files, files, d
84
+
85
+ def create_hand_record(files):
86
+ file_path = create_hand_record_pdf(files)
87
+ return file_path
88
 
89
  with gr.Blocks(css=custom_css) as demo:
90
  gr.Markdown(
 
95
  The results can be exported as a PBN file, which can be imported to other bridge software such as double dummy solvers.
96
  1. Upload an image showing all four hands fanned as shown in the example.
97
  2. Click *Submit*. The scan result will be displayed in the table.
98
+ 3. Verify the output and correct any missing or wrong card in the table.
99
+ 4. Enter the information of the deal.
100
+ 5. Click *Save* to generate a PBN file.
101
+ 6. In the tab *Hand Record*, You can upload all the PBN files and create a hand record as a PDF file.
102
 
103
  Tips:
104
+ - This AI reads the values at corners of the playing cards. Make sure they are visible and as large as possible.
105
  - To get the best accuracy, place the cards following the layout in the examples.
106
 
107
+ Please send your comments to <vincentlui123@gmail.com>.
108
  """)
109
 
110
  total = gr.State(0)
111
  gradio_cache_dir = gr.State()
112
+ files = gr.State([])
113
+ with gr.Tab('Scan Image'):
114
+ with gr.Row():
115
+ with gr.Column():
116
+ a1 = gr.Image(type="filepath",sources=['upload'],interactive=True,height=INPUT_IMG_HEIGHT)
117
+ with gr.Row():
118
+ a2 = gr.ClearButton()
119
+ a3 = gr.Button('Submit',variant="primary")
120
+ with gr.Accordion("Board Details",open=True):
121
+ with gr.Row():
122
+ a_board_no = gr.Number(label="Board", value=1, minimum=1, maximum=999, interactive=True, min_width=80)
123
+ a_top = gr.Dropdown(['N','E','S','W'], label='Top', value='N', interactive=True, min_width=80, type='index')
124
+ a_deck = gr.Dropdown(['Standard (AKQJ)'], label='Deck',
125
+ value='Standard (AKQJ)', type='index', interactive=True, min_width=80, scale=2)
126
+ # with gr.Accordion("Contract Details",open=False) as a_c:
127
+ # with gr.Row():
128
+ # with gr.Column(scale=3):
129
+ # with gr.Group():
130
+ # a_level = gr.Radio(['1','2','3','4','5','6','7','AP'], label='Contract')
131
+ # a_trump = gr.Radio(['♠', '♥', '♦', '♣', 'NT'], show_label=False)
132
+ # a_dbl = gr.Radio(['X', 'XX'], show_label=False)
133
+ # a_declarer = gr.Radio(['N','E','S','W'], label='Declarer', min_width=80, scale=1)
134
+ a4 = gr.Examples('examples', a1)
135
+
136
+ with gr.Column():
137
+ b1 = gr.Dataframe(value=default_df, datatype="str", row_count=(4,'fixed'), col_count=(5,'fixed'),
138
+ headers=['', 'N', 'E', 'S', 'W'],
139
+ interactive=True, column_widths=['8%', '23%','23%','23%','23%'])
140
+ b2 = gr.Button('Save')
141
+ b3 = gr.File(interactive=False, file_count='multiple')
142
+
143
+ with gr.Tab('Hand Record'):
144
+ with gr.Row():
145
+ with gr.Group():
146
+ tab2_upload_file = gr.Files(interactive=True)
147
+ tab2_submit_button = gr.Button('Create Hand Record',variant="primary")
148
+ tab2_download_file = gr.File(interactive=False)
149
+
150
+ tab2_submit_button.click(create_hand_record, tab2_upload_file, tab2_download_file)
151
+
152
+ a2.add([a1,b1])
153
+ a3.click(predict, [a1, a_top], [b1])
154
+ b2.click(save_file, [b1, gradio_cache_dir, files, a_board_no], [b3, tab2_upload_file, gradio_cache_dir])
155
 
156
  demo.queue().launch()
hand_record.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fpdf import FPDF
2
+ import tempfile
3
+ import os
4
+ import bridgebots
5
+ from pbn_util import merge_pbn, parse_pbn
6
+
7
+ DEALER_LIST = ['N', 'E', 'S', 'W']
8
+ VULNERABILITY_LIST = ["None","NS","EW","All","NS","EW","All","None","EW","All","None","NS","All","None","NS","EW"]
9
+ SUITS = [bridgebots.Suit.SPADES, bridgebots.Suit.HEARTS, bridgebots.Suit.DIAMONDS, bridgebots.Suit.CLUBS]
10
+ SUIT_SYMBOLS = ['♠','♥','♦','♣']
11
+
12
+ def create_hand_record_pdf(pbn_paths):
13
+ filepath_merged_pbn = merge_pbn(pbn_paths)
14
+ results = parse_pbn(filepath_merged_pbn)
15
+ fd,fn = tempfile.mkstemp(".pdf")
16
+ pdf = FPDF()
17
+
18
+ pdf.add_page()
19
+ pdf.add_font('times2', style='', fname='times.ttf')
20
+ pdf.set_font("times2", "", 8)
21
+ pdf.c_margin = 0.1
22
+ table_config = {
23
+ 'borders_layout':'NONE',
24
+ 'col_widths':3,
25
+ 'line_height':pdf.font_size + 0.5,
26
+ 'align': 'L',
27
+ 'text_align': 'L',
28
+ 'first_row_as_headings': False,
29
+ }
30
+
31
+ start_x,start_y = 10,10
32
+ table_size = 45, 48
33
+ table_margin = 2
34
+ for i, result in enumerate(results):
35
+ deal = result[0]
36
+ board_no = int(result[1][0]['Board'])
37
+ page_i = i % 20
38
+ if (i % 20 == 0) and (i != 0):
39
+ pdf.add_page()
40
+ row_idx = page_i // 4
41
+ col_idx = page_i % 4
42
+ x = start_x + (table_size[0] + 2 * table_margin + 1) * col_idx
43
+ y = start_y + (table_size[1] + 2 * table_margin + 1) * row_idx
44
+
45
+ top_left = x - table_margin, y - table_margin
46
+ top_right = x + table_size[0] + table_margin, y - table_margin
47
+ bottom_left = x - table_margin, y + table_size[1] + table_margin
48
+ bottom_right = x + table_size[0] + table_margin, y + table_size[1] + table_margin
49
+ pdf.set_xy(x,y)
50
+
51
+ pdf.line(*top_left, *bottom_left)
52
+ pdf.line(*top_left, * top_right)
53
+ pdf.line(*top_right, *bottom_right)
54
+ pdf.line(*bottom_left, *bottom_right)
55
+
56
+ dealer = DEALER_LIST[(board_no-1) % 4]
57
+ vul = VULNERABILITY_LIST[(board_no-1) % 16]
58
+ with pdf.table(**table_config) as table:
59
+ row = table.row()
60
+ row.cell(f'{board_no}\n{dealer}/{vul}', colspan=6, rowspan=4, align='C', v_align='C')
61
+ for i, (suit, values) in enumerate(zip(SUIT_SYMBOLS, get_values(deal, 'N'))):
62
+ if i!=0:
63
+ row = table.row()
64
+ print_suit_values(pdf,row,suit,values)
65
+
66
+ row = table.row()
67
+ # row = table.row()
68
+ for i, (suit, values1, values2) in enumerate(zip(SUIT_SYMBOLS, get_values(deal, 'W'), get_values(deal, 'E'))):
69
+ row = table.row()
70
+ print_suit_values(pdf, row, suit, values1)
71
+ row.cell('',colspan=5)
72
+
73
+ print_suit_values(pdf, row, suit, values2)
74
+
75
+ row = table.row()
76
+ row = table.row()
77
+ row.cell('', colspan=6, rowspan=4)
78
+ for i, (suit, values) in enumerate(zip(SUIT_SYMBOLS, get_values(deal, 'S'))):
79
+ if i!=0:
80
+ row = table.row()
81
+ print_suit_values(pdf, row, suit, values)
82
+
83
+ pdf.output(fn)
84
+ return fn
85
+
86
+
87
+ def get_values(result, direction):
88
+ d = bridgebots.Direction.from_str(direction)
89
+ hand = result.hands[d]
90
+ return [''.join([value.abbreviation()
91
+ for value in hand.suits[suit]])
92
+ for suit in SUITS]
93
+
94
+ def print_suit_values(pdf, row, suit, values):
95
+ if (suit=='♥') or (suit=='♦'): # Hearts or Diamonds
96
+ pdf.set_text_color(255,0,0)
97
+ row.cell(suit, colspan=1)
98
+ pdf.set_text_color(0,0,0)
99
+ row.cell(values, colspan=4)
pbn_util.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ import logging
3
+ import bridgebots
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+ import pandas as pd
7
+ import tempfile
8
+ from collections import Counter
9
+
10
+
11
+ DEALER_LIST = ['N', 'E', 'S', 'W']
12
+ VULNERABILITY_LIST = ["None","NS","EW","All","NS","EW","All","None","EW","All","None","NS","All","None","NS","EW"]
13
+ SUITS = [bridgebots.Suit.SPADES, bridgebots.Suit.HEARTS, bridgebots.Suit.DIAMONDS, bridgebots.Suit.CLUBS]
14
+
15
+ def parse_single_pbn_record(record_strings):
16
+ """
17
+ :param record_strings: One string per line of a single PBN deal record
18
+ :return: Deal and BoardRecord corresponding to the PBN record
19
+ """
20
+ record_dict = bridgebots.pbn._build_record_dict(record_strings)
21
+ try:
22
+ deal = bridgebots.pbn.from_pbn_deal(record_dict["Dealer"], record_dict["Vulnerable"], record_dict["Deal"])
23
+ except KeyError as e:
24
+ # if previous_deal:
25
+ # deal = previous_deal
26
+ # else:
27
+ raise ValueError("Missing deal fields and no previous_deal provided") from e
28
+ # board_record = _parse_board_record(record_dict, deal)
29
+ return deal, record_dict
30
+
31
+
32
+ def parse_pbn(file_path):
33
+ """
34
+ Split PBN file into boards then decompose those boards into Deal and BoardRecord objects. Only supports PBN v1.0
35
+ See https://www.tistis.nl/pbn/pbn_v10.txt
36
+
37
+ :param file_path: path to a PBN file
38
+ :return: A list of DealRecords representing all the boards played
39
+ """
40
+ records_strings = bridgebots.pbn._split_pbn(file_path)
41
+ # Maintain a mapping from deal to board records to create a single deal record per deal
42
+ records = defaultdict(list)
43
+ # Some PBNs have multiple board records per deal
44
+ previous_deal = None
45
+ for record_strings in records_strings:
46
+ try:
47
+ deal, board_record = parse_single_pbn_record(record_strings)
48
+ records[deal].append(board_record)
49
+ # previous_deal = deal
50
+ except (KeyError, ValueError) as e:
51
+ logging.warning(f"Malformed record {record_strings}: {e}")
52
+ return [(deal, board_records) for deal, board_records in records.items()]
53
+
54
+
55
+ def create_single_pbn_string(
56
+ data: pd.DataFrame,
57
+ date=datetime.today(),
58
+ board_no=1,
59
+ event='',
60
+ site='',
61
+ ) -> str:
62
+ year = date.strftime("%y")
63
+ month = date.strftime("%m")
64
+ day = date.strftime("%d")
65
+ date_print = day + "." + month + "." + year
66
+ dealer = DEALER_LIST[(board_no-1) % 4]
67
+ vulnerability=VULNERABILITY_LIST[(board_no-1) % 16]
68
+ deal = 'N:'
69
+ deal += ' '.join(
70
+ ['.'.join(data[col]) for col in data.columns[1:]]
71
+ ) # sss.hhh.ddd.ccc sss.hhh.ddd.ccc......
72
+
73
+ file = ''
74
+ file += ("%This pbn was generated by Bridge Hand Scanner\n")
75
+ file += f'[Event "{event}"]\n'
76
+ file += f'[Site "{site}"]\n'
77
+ file += f'[Date "{date_print}"]\n'
78
+ file += f'[Board "{str(board_no)}"]\n'
79
+ file += f'[Dealer "{dealer}"]\n'
80
+ file += f'[Vulnerable "{vulnerability}"]\n'
81
+ file += f'[Deal "{deal}"]\n'
82
+
83
+ return file
84
+
85
+ def merge_pbn(pbn_paths):
86
+ fd, fn = tempfile.mkstemp(suffix='.pbn', text=True)
87
+ board_dict = {}
88
+ for i, pbn_path in enumerate(pbn_paths):
89
+ result = parse_pbn(pbn_path)[0]
90
+ with open(pbn_path, 'r') as f2:
91
+ pbn_str = f2.read()
92
+ board_no = result[1][0]['Board']
93
+ board_dict[board_no] = pbn_str
94
+
95
+ ordered_board_dict = dict(sorted(board_dict.items()))
96
+ with open(fd, 'w') as f:
97
+ for i, (k,v) in enumerate(ordered_board_dict.items()):
98
+ if i != 0:
99
+ f.write('\n*\n')
100
+ f.write(v)
101
+ return fn
102
+
103
+
104
+ def validate_pbn(pbn_string):
105
+ try:
106
+ deal, record = parse_single_pbn_record(pbn_string)
107
+ except AssertionError:
108
+ raise ValueError('Everyone should have 13 cards')
109
+ except Exception as e:
110
+ print('test')
111
+ raise Exception(e)
112
+ hands = deal.hands
113
+ duplicated = set()
114
+ missing = set()
115
+ validation_dict = {}
116
+ for suit in bridgebots.Suit:
117
+ cards = [hands[direction].suits[suit] for direction in hands]
118
+ # assert len(cards) == 13,
119
+ cards = [c for c in sum([hands[direction].suits[suit] for direction in hands], [])]
120
+ duplicated.update([bridgebots.Card(suit,val) for val,cnt in Counter(cards).items() if cnt >1])
121
+
122
+ cards_set = set(cards)
123
+ missing.update([bridgebots.Card(suit,r) for r in bridgebots.Rank if not r in cards_set])
124
+
125
+ err_msg = ''
126
+ if len(duplicated) > 0:
127
+ err_msg += f'Duplicated cards: {duplicated}. '
128
+ if len(missing) > 0:
129
+ err_msg += f'Missing cards: {missing}. '
130
+
131
+ for direction in hands:
132
+ num_cards = len(hands[direction].cards)
133
+ if not num_cards == 13:
134
+ err_msg += '{direction.name} has {num_cards} cards. '
135
+
136
+ if err_msg:
137
+ raise ValueError(err_msg)
requirements.txt CHANGED
@@ -4,4 +4,5 @@ onnxruntime
4
  lap
5
  opencv-python
6
  openvino==2024.0.0
7
- albumentations
 
 
4
  lap
5
  opencv-python
6
  openvino==2024.0.0
7
+ albumentations
8
+ fpdf2