591 lines
22 KiB
Python
591 lines
22 KiB
Python
"""
|
|
GGPoker Analyzer
|
|
webcam -> calibrated zones -> OCR cards -> dealer detection -> Monte Carlo equity
|
|
"""
|
|
import sys, json, cv2, numpy as np
|
|
from PIL import Image
|
|
|
|
sys.path.insert(0, '/home/isaevea/python/dickreuter_poker')
|
|
sys.path.insert(0, '/home/isaevea/python/pp')
|
|
|
|
from types import ModuleType
|
|
_stub = ModuleType('poker.scraper.table_setup_actions_and_signals')
|
|
_stub.CARD_VALUES = "23456789TJQKA"; _stub.CARD_SUITES = "CDHS"
|
|
sys.modules['poker.scraper.table_setup_actions_and_signals'] = _stub
|
|
|
|
CALIB_FILE = '/home/isaevea/python/pp/calibration.json'
|
|
FONT = cv2.FONT_HERSHEY_SIMPLEX
|
|
DETECT_EVERY = 8
|
|
LOCK_NEEDED = 5
|
|
|
|
RANK_MAP = {'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,
|
|
'9':9,'T':10,'J':11,'Q':12,'K':13,'A':14}
|
|
TEMPLATES_DIR = '/home/isaevea/python/pp/templates'
|
|
CARD_SIZE = (60, 80) # размер для сравнения полной карты
|
|
CORNER_SIZE = (30, 50)
|
|
RANKS = ['2','3','4','5','6','7','8','9','T','J','Q','K','A']
|
|
SUITS = ['c','d','h','s']
|
|
|
|
_card_templates = None
|
|
def load_card_templates():
|
|
global _card_templates
|
|
if _card_templates is not None: return _card_templates
|
|
_card_templates = {}
|
|
for r in RANKS:
|
|
for s in SUITS:
|
|
key = r + s
|
|
img = cv2.imread(f'{TEMPLATES_DIR}/{key}.png', cv2.IMREAD_GRAYSCALE)
|
|
if img is not None:
|
|
_card_templates[key] = cv2.resize(img, CARD_SIZE)
|
|
print(f'[+] Loaded {len(_card_templates)} card templates')
|
|
return _card_templates
|
|
SUIT_MAP = {'c':0,'d':1,'h':2,'s':3}
|
|
SUIT_SYM = {'h':'♥','d':'♦','c':'♣','s':'♠',
|
|
'H':'♥','D':'♦','C':'♣','S':'♠'}
|
|
POS_NAMES = {0:'Hero(BTN?)',1:'P1',2:'P2',3:'P3',4:'P4',5:'P5',6:'P6',7:'P7'}
|
|
|
|
|
|
# ── Load calibration ──────────────────────────────────────────────────────────
|
|
def load_calib():
|
|
with open(CALIB_FILE) as f:
|
|
return json.load(f)
|
|
|
|
def zone(calib, key):
|
|
v = calib.get(key)
|
|
return (v['x1'],v['y1'],v['x2'],v['y2']) if v else None
|
|
|
|
def crop(frame, key, calib):
|
|
r = zone(calib, key)
|
|
if r is None: return None
|
|
x1,y1,x2,y2 = r
|
|
return frame[y1:y2, x1:x2]
|
|
|
|
|
|
# ── Camera ────────────────────────────────────────────────────────────────────
|
|
def open_camera():
|
|
cap = cv2.VideoCapture(2)
|
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
|
for _ in range(20): cap.read()
|
|
return cap
|
|
|
|
|
|
# ── OCR rank ─────────────────────────────────────────────────────────────────
|
|
_tess = None
|
|
def get_tess():
|
|
global _tess
|
|
if _tess is None:
|
|
from tesserocr import PyTessBaseAPI, PSM, OEM
|
|
_tess = PyTessBaseAPI(
|
|
path='/home/isaevea/python/dickreuter_poker/tessdata',
|
|
psm=PSM.SINGLE_CHAR, oem=OEM.LSTM_ONLY)
|
|
_tess.SetVariable('tessedit_char_whitelist','23456789TJQKAtjqka')
|
|
return _tess
|
|
|
|
_OCR_FIX = {'R':'K','I':'J','0':'Q','O':'Q','1':'J','L':'J','B':'8','G':'9','Z':'2'}
|
|
|
|
def _ocr_region(img_bgr):
|
|
"""Run Tesseract on a BGR image region, return rank char or '?'."""
|
|
try:
|
|
from tesserocr import PyTessBaseAPI, PSM, OEM
|
|
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
|
|
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
|
up = cv2.resize(thresh, (0,0), fx=3, fy=3, interpolation=cv2.INTER_LANCZOS4)
|
|
pil = Image.fromarray(up)
|
|
with PyTessBaseAPI(
|
|
path='/home/isaevea/python/dickreuter_poker/tessdata',
|
|
psm=PSM.SINGLE_WORD, oem=OEM.LSTM_ONLY
|
|
) as api:
|
|
api.SetVariable('tessedit_char_whitelist', '23456789TJQKAtjqka10')
|
|
api.SetImage(pil)
|
|
r = api.GetUTF8Text().strip().upper().replace(' ','')
|
|
if not r: return '?'
|
|
if r.startswith('10') or r == '1O': return 'T'
|
|
c = r[0]
|
|
c = _OCR_FIX.get(c, c)
|
|
if c in '23456789TJQKA': return c
|
|
return '?'
|
|
except:
|
|
return '?'
|
|
|
|
def ocr_big_rank(img_bgr):
|
|
"""Try lower-right (large rank) first, fallback to upper-left (small rank)."""
|
|
h, w = img_bgr.shape[:2]
|
|
# large central rank (board cards style)
|
|
r = _ocr_region(img_bgr[int(h*0.35):, int(w*0.30):])
|
|
if r != '?':
|
|
return r
|
|
# upper portion (hero cards show small rank at top)
|
|
r = _ocr_region(img_bgr[:int(h*0.55), :int(w*0.65)])
|
|
return r
|
|
|
|
def match_card(img_bgr):
|
|
"""
|
|
Rank: OCR (large central digit — handles '10' correctly).
|
|
Suit: shape template matching, fallback to color detection.
|
|
"""
|
|
if not is_card_present(img_bgr): return '??'
|
|
rank = ocr_big_rank(img_bgr)
|
|
suit = detect_suit_by_shape(img_bgr)
|
|
if suit == '?':
|
|
suit = detect_suit_ggpoker(img_bgr)
|
|
if rank == '?' or suit == '?': return '??'
|
|
return rank + suit
|
|
|
|
def ocr_rank(img_bgr): return '?'
|
|
|
|
|
|
# ── Suit detection ────────────────────────────────────────────────────────────
|
|
_suit_templates = None
|
|
def load_suit_templates():
|
|
global _suit_templates
|
|
if _suit_templates is not None: return _suit_templates
|
|
_suit_templates = {}
|
|
for s in SUITS:
|
|
img = cv2.imread(f'{TEMPLATES_DIR}/suit_{s}.png', cv2.IMREAD_GRAYSCALE)
|
|
if img is not None:
|
|
_suit_templates[s] = cv2.resize(img, (30, 30))
|
|
return _suit_templates
|
|
|
|
_rank_shape_templates = None
|
|
def load_rank_shape_templates():
|
|
global _rank_shape_templates
|
|
if _rank_shape_templates is not None: return _rank_shape_templates
|
|
_rank_shape_templates = {}
|
|
for r in RANKS:
|
|
img = cv2.imread(f'{TEMPLATES_DIR}/rank_{r}.png', cv2.IMREAD_GRAYSCALE)
|
|
if img is not None:
|
|
_rank_shape_templates[r] = cv2.resize(img, (40, 50))
|
|
print(f'[+] Loaded {len(_rank_shape_templates)} rank templates')
|
|
return _rank_shape_templates
|
|
|
|
def detect_suit_by_shape(img_bgr):
|
|
"""Match suit symbol area against 4 templates from card files."""
|
|
tpls = load_suit_templates()
|
|
if not tpls: return '?'
|
|
h, w = img_bgr.shape[:2]
|
|
sym = img_bgr[int(h*0.30):int(h*0.52), :int(w*0.42)]
|
|
gray = cv2.cvtColor(sym, cv2.COLOR_BGR2GRAY)
|
|
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
binary_r = cv2.resize(binary, (30, 30))
|
|
best_suit, best_score = '?', -1.0
|
|
for s, tpl in tpls.items():
|
|
score = float(cv2.matchTemplate(binary_r, tpl, cv2.TM_CCOEFF_NORMED).max())
|
|
if score > best_score:
|
|
best_score, best_suit = score, s
|
|
return best_suit if best_score > 0.15 else '?'
|
|
|
|
def match_rank_by_shape(img_bgr, suit=None):
|
|
"""Match rank — top 50% full width for better coverage of large digits."""
|
|
tpls = load_rank_shape_templates()
|
|
if not tpls: return '?'
|
|
h, w = img_bgr.shape[:2]
|
|
rank_area = img_bgr[:int(h*0.50), :] # top half, full width
|
|
gray = cv2.cvtColor(rank_area, cv2.COLOR_BGR2GRAY)
|
|
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
binary_r = cv2.resize(binary, (40, 50))
|
|
best_rank, best_score = '?', -1.0
|
|
for r, tpl in tpls.items():
|
|
score = float(cv2.matchTemplate(binary_r, tpl, cv2.TM_CCOEFF_NORMED).max())
|
|
if score > best_score:
|
|
best_score, best_rank = score, r
|
|
return best_rank if best_score > 0.15 else '?'
|
|
|
|
def detect_suit_ggpoker(img_bgr):
|
|
"""
|
|
GGPoker colored bg: Red=h, Green=c, Blue=d, Dark=s — fast color check.
|
|
White bg (classic): fall through to shape matching.
|
|
"""
|
|
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
|
|
h_ch, s_ch, v_ch = cv2.split(hsv)
|
|
|
|
ch, cw = img_bgr.shape[:2]
|
|
cy1, cy2 = int(ch*0.20), int(ch*0.90)
|
|
cx1, cx2 = int(cw*0.10), int(cw*0.90)
|
|
hc = h_ch[cy1:cy2, cx1:cx2]
|
|
sc = s_ch[cy1:cy2, cx1:cx2]
|
|
vc = v_ch[cy1:cy2, cx1:cx2]
|
|
|
|
mean_v = float(np.mean(vc))
|
|
mean_s = float(np.mean(sc))
|
|
|
|
# GGPoker dark background → spades
|
|
if mean_v < 130 and mean_s > 30:
|
|
return 's'
|
|
|
|
# White/light background → use shape matching
|
|
if mean_v > 155 and mean_s < 55:
|
|
return detect_suit_by_shape(img_bgr)
|
|
|
|
# GGPoker colored background → color buckets
|
|
mask = (sc > 60) & (vc > 60)
|
|
if mask.sum() < 30:
|
|
return detect_suit_by_shape(img_bgr)
|
|
|
|
hues = hc[mask]
|
|
red = int(np.sum((hues < 12) | (hues > 165)))
|
|
green = int(np.sum((hues >= 38) & (hues <= 88)))
|
|
blue = int(np.sum((hues >= 89) & (hues <= 148)))
|
|
|
|
best = max(red, green, blue)
|
|
if best < 10:
|
|
return detect_suit_by_shape(img_bgr)
|
|
if best == red: return 'h'
|
|
if best == green: return 'c'
|
|
return 'd'
|
|
|
|
detect_suit_board = detect_suit_ggpoker
|
|
detect_suit_hero = detect_suit_ggpoker
|
|
|
|
def is_card_present(img_bgr):
|
|
if img_bgr is None or img_bgr.size == 0: return False
|
|
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
|
|
return float(np.std(gray)) > 18
|
|
|
|
|
|
# ── Card reading ──────────────────────────────────────────────────────────────
|
|
def read_card(img_bgr, is_board=True):
|
|
if not is_card_present(img_bgr): return '??'
|
|
return match_card(img_bgr)
|
|
|
|
def read_board(frame, calib):
|
|
cards = []
|
|
for i in range(1, 6):
|
|
img = crop(frame, f'board_card_{i}', calib)
|
|
if img is None: break
|
|
c = read_card(img, is_board=True)
|
|
if c == '??': break
|
|
cards.append(c)
|
|
return cards
|
|
|
|
def read_hero(frame, calib):
|
|
cards = []
|
|
for i in range(1, 3):
|
|
img = crop(frame, f'hero_card_{i}', calib)
|
|
c = read_card(img, is_board=False) if img is not None else '??'
|
|
cards.append(c)
|
|
return cards
|
|
|
|
|
|
# ── Dealer button detection ───────────────────────────────────────────────────
|
|
_dealer_tpl = None
|
|
def load_dealer_template():
|
|
global _dealer_tpl
|
|
if _dealer_tpl is None:
|
|
img = cv2.imread('/home/isaevea/python/pp/files/D.png', cv2.IMREAD_COLOR)
|
|
if img is not None:
|
|
_dealer_tpl = img
|
|
return _dealer_tpl
|
|
|
|
def find_dealer(frame, calib):
|
|
"""
|
|
Match D.png template in each player zone.
|
|
Returns player index or None.
|
|
"""
|
|
tpl = load_dealer_template()
|
|
best_player, best_score = None, 0.0
|
|
th, tw = (tpl.shape[:2] if tpl is not None else (0,0))
|
|
|
|
for i in range(8):
|
|
img = crop(frame, f'player_{i}', calib)
|
|
if img is None: continue
|
|
h, w = img.shape[:2]
|
|
|
|
if tpl is not None and w >= tw and h >= th:
|
|
# try at multiple scales
|
|
for scale in [0.5, 0.7, 1.0, 1.3]:
|
|
sw2, sh2 = max(1,int(tw*scale)), max(1,int(th*scale))
|
|
if sw2 > w or sh2 > h: continue
|
|
t = cv2.resize(tpl, (sw2, sh2))
|
|
res = cv2.matchTemplate(img, t, cv2.TM_CCOEFF_NORMED)
|
|
score = float(res.max())
|
|
if score > best_score:
|
|
best_score, best_player = score, i
|
|
else:
|
|
# fallback: color detection
|
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
|
mask = cv2.inRange(hsv, (12,120,150),(38,255,255))
|
|
score = float(np.sum(mask>0)) / max(1, mask.size)
|
|
if score > best_score:
|
|
best_score, best_player = score, i
|
|
|
|
return best_player if best_score > 0.35 else None
|
|
|
|
|
|
# ── Monte Carlo ───────────────────────────────────────────────────────────────
|
|
def to_mc(card):
|
|
if not card or len(card)<2 or '?' in card: return None
|
|
r = RANK_MAP.get(card[0].upper())
|
|
s = SUIT_MAP.get(card[1].lower())
|
|
return [r,s] if r and s is not None else None
|
|
|
|
def calc_equity(hero, board, n_players=4, iters=3000):
|
|
from poker.decisionmaker.montecarlo_numpy2 import Evaluation
|
|
c1,c2 = to_mc(hero[0]), to_mc(hero[1])
|
|
if not c1 or not c2: return None
|
|
table = [c for c in (to_mc(b) for b in board) if c]
|
|
try: return Evaluation().run_evaluation(c1,c2,table,iters,n_players)
|
|
except: return None
|
|
|
|
def advice(eq):
|
|
if eq > 0.65: return 'RAISE / BET'
|
|
if eq > 0.45: return 'CALL / CHECK'
|
|
if eq > 0.25: return 'CALL (осторожно)'
|
|
return 'FOLD'
|
|
|
|
def position_name(dealer_idx, player_idx, n=8):
|
|
"""Determine position name relative to dealer."""
|
|
if dealer_idx is None: return '?'
|
|
offset = (player_idx - dealer_idx) % n
|
|
names = {0:'BTN',1:'SB',2:'BB',3:'UTG',4:'UTG+1',5:'MP',6:'HJ',7:'CO'}
|
|
return names.get(offset, f'+{offset}')
|
|
|
|
|
|
# ── Overlay ───────────────────────────────────────────────────────────────────
|
|
def put(img, text, pos, scale, color, thick=2):
|
|
x,y=pos
|
|
cv2.putText(img,text,(x+1,y+1),FONT,scale,(0,0,0),thick+1)
|
|
cv2.putText(img,text,(x,y), FONT,scale,color, thick)
|
|
|
|
def card_color(suit):
|
|
return (60,60,255) if suit.lower() in('h','d') else (230,230,230)
|
|
|
|
def draw_card(img, card, x, y, w=56, h=78):
|
|
s = card[1].lower() if len(card)>1 else ''
|
|
bg = (30,30,160) if s in('h','d') else (20,20,20)
|
|
cv2.rectangle(img,(x,y),(x+w,y+h),bg,-1)
|
|
cv2.rectangle(img,(x,y),(x+w,y+h),(160,160,160),1)
|
|
if '?' not in card and len(card)==2:
|
|
col = card_color(card[1])
|
|
put(img, card[0].upper(), (x+5,y+30), 0.9, col, 2)
|
|
put(img, SUIT_SYM.get(card[1],'?'), (x+5,y+56), 0.75, col, 2)
|
|
else:
|
|
put(img,'?',(x+18,y+46),1.0,(80,80,80),2)
|
|
|
|
def draw_overlay(frame, state, calib):
|
|
ph,pw = frame.shape[:2]
|
|
PX,PY,PW,PH = 8, ph-230, 560, 220
|
|
|
|
ov = frame.copy()
|
|
cv2.rectangle(ov,(PX,PY),(PX+PW,PY+PH),(15,15,15),-1)
|
|
cv2.addWeighted(ov,0.78,frame,0.22,0,frame)
|
|
cv2.rectangle(frame,(PX,PY),(PX+PW,PY+PH),(70,70,70),1)
|
|
|
|
y = PY+28
|
|
put(frame,'BOARD',(PX+8,y),0.52,(150,150,150))
|
|
for i,c in enumerate(state['board']+['??']*(5-len(state['board']))):
|
|
draw_card(frame, c if i<len(state['board']) else '??', PX+82+i*62, y-20)
|
|
|
|
y += 90
|
|
put(frame,'HERO',(PX+8,y),0.52,(150,150,150))
|
|
for i,c in enumerate(state['hero']):
|
|
draw_card(frame, c, PX+82+i*62, y-20)
|
|
|
|
y += 90
|
|
eq = state.get('equity')
|
|
if eq is not None:
|
|
pct = eq*100
|
|
col = (0,210,0) if pct>55 else (0,180,255) if pct>35 else (50,50,230)
|
|
put(frame,f'Equity {pct:.0f}% {state["advice"]}',(PX+8,y),0.65,col,2)
|
|
else:
|
|
put(frame,'Жду карты...',(PX+8,y),0.55,(100,100,100))
|
|
|
|
# dealer / position
|
|
dealer = state.get('dealer')
|
|
pos = state.get('hero_position','?')
|
|
if dealer is not None or pos != '?':
|
|
info = f'Dealer: P{dealer} Hero pos: {pos}' if dealer is not None else f'Hero pos: {pos}'
|
|
put(frame,info,(PX+8,PY+PH-10),0.48,(200,200,80))
|
|
|
|
# lock dot
|
|
locked = state.get('locked',False)
|
|
cv2.circle(frame,(PX+PW-14,PY+14),6,(0,200,0) if locked else (0,120,220),-1)
|
|
|
|
# draw calibration zones scaled to display resolution
|
|
dh, dw = frame.shape[:2]
|
|
sx, sy = dw/1920, dh/1080
|
|
for key,v in calib.items():
|
|
if 'board' in key: col=(0,180,0)
|
|
elif 'hero' in key: col=(0,140,255)
|
|
elif 'player' in key: col=(180,0,180)
|
|
else: continue
|
|
x1,y1 = int(v['x1']*sx), int(v['y1']*sy)
|
|
x2,y2 = int(v['x2']*sx), int(v['y2']*sy)
|
|
cv2.rectangle(frame,(x1,y1),(x2,y2),col,1)
|
|
cv2.putText(frame, key.split('_')[-1], (x1+2,y1+12),
|
|
FONT, 0.30, col, 1)
|
|
|
|
|
|
# ── Vote buffer ───────────────────────────────────────────────────────────────
|
|
class VoteBuf:
|
|
def __init__(self,n=LOCK_NEEDED):
|
|
self.n=n; self.h=[]
|
|
def push(self,val):
|
|
self.h.append(str(val))
|
|
if len(self.h)>self.n*2: self.h=self.h[-self.n*2:]
|
|
if len(self.h)>=self.n and len(set(self.h[-self.n:]))==1:
|
|
self.h=[]; return val
|
|
return None
|
|
def reset(self): self.h=[]
|
|
|
|
|
|
SUIT_FULL = {'h':'Hearts','d':'Diamonds','c':'Clubs','s':'Spades',
|
|
'H':'Hearts','D':'Diamonds','C':'Clubs','S':'Spades'}
|
|
|
|
def draw_info_panel(state):
|
|
"""Draw a standalone text info window."""
|
|
W, H = 380, 320
|
|
img = np.zeros((H, W, 3), dtype=np.uint8)
|
|
img[:] = (25, 25, 25)
|
|
|
|
def row(text, y, color=(200,200,200), scale=0.65, thick=1):
|
|
cv2.putText(img, text, (12, y), FONT, scale, color, thick)
|
|
|
|
row('GGPoker Analyzer', 28, (100,200,100), 0.70, 2)
|
|
cv2.line(img, (10,36), (W-10,36), (60,60,60), 1)
|
|
|
|
SUIT_LETTER = {'h':'h','d':'d','c':'c','s':'s',
|
|
'H':'h','D':'d','C':'c','S':'s'}
|
|
|
|
def fmt_card(c):
|
|
if not c or '??' in c or len(c) < 2: return '??'
|
|
return f"{c[0].upper()}{SUIT_LETTER.get(c[1],'?')}"
|
|
|
|
# Board cards
|
|
row('BOARD:', 62, (150,150,150), 0.55)
|
|
board = state.get('board', [])
|
|
btext = ' '.join(fmt_card(c) for c in board) if board else '---'
|
|
row(btext, 90, (255,255,255), 0.80, 2)
|
|
|
|
# Hero cards
|
|
row('HERO:', 120, (150,150,150), 0.55)
|
|
hero = state.get('hero', ['??','??'])
|
|
htext = ' '.join(fmt_card(c) for c in hero)
|
|
row(htext, 148, (100,200,255), 0.80, 2)
|
|
|
|
cv2.line(img, (10,162), (W-10,162), (60,60,60), 1)
|
|
|
|
# Equity + advice
|
|
eq = state.get('equity')
|
|
if eq is not None:
|
|
pct = eq * 100
|
|
col = (0,210,0) if pct>55 else (0,180,255) if pct>35 else (60,60,240)
|
|
row(f'Equity: {pct:.1f}%', 192, col, 0.75, 2)
|
|
row(state.get('advice',''), 222, col, 0.70, 2)
|
|
else:
|
|
row('Waiting for cards...', 200, (80,80,80), 0.60)
|
|
|
|
cv2.line(img, (10,240), (W-10,240), (60,60,60), 1)
|
|
|
|
# Position
|
|
pos = state.get('hero_position','?')
|
|
dealer = state.get('dealer')
|
|
row(f'Position: {pos}', 266,
|
|
(200,200,80) if pos != '?' else (80,80,80), 0.65, 1)
|
|
if dealer is not None:
|
|
row(f'Dealer at: P{dealer}', 292, (150,150,150), 0.50)
|
|
|
|
# Lock indicator
|
|
locked = state.get('locked', False)
|
|
dot_col = (0,200,0) if locked else (0,100,200)
|
|
cv2.circle(img, (W-20, 20), 7, dot_col, -1)
|
|
lbl = 'LOCKED' if locked else 'SCAN'
|
|
cv2.putText(img, lbl, (W-70, 16), FONT, 0.38, dot_col, 1)
|
|
|
|
# Hint
|
|
row('[R]=reset [Q/ESC]=quit', 310, (50,50,50), 0.38)
|
|
return img
|
|
|
|
|
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
def main():
|
|
calib = load_calib()
|
|
cap = open_camera()
|
|
|
|
WIN_CAM = 'Webcam'
|
|
WIN_INFO = 'Info'
|
|
|
|
cv2.namedWindow(WIN_CAM, cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL)
|
|
cv2.namedWindow(WIN_INFO, cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL)
|
|
cv2.resizeWindow(WIN_CAM, 960, 540)
|
|
cv2.resizeWindow(WIN_INFO, 380, 320)
|
|
|
|
board_buf = VoteBuf(); hero_buf = VoteBuf()
|
|
state = {'board':[],'hero':['??','??'],'equity':None,
|
|
'advice':'','dealer':None,'hero_position':'?','locked':False}
|
|
fn = 0
|
|
|
|
ret, frame = cap.read()
|
|
if ret:
|
|
cv2.imshow(WIN_CAM, cv2.resize(frame,(960,540)))
|
|
cv2.imshow(WIN_INFO, draw_info_panel(state))
|
|
cv2.waitKey(1)
|
|
|
|
print('[+] Running. R=reset, ESC/Q=quit')
|
|
|
|
while True:
|
|
ret, frame = cap.read()
|
|
if not ret: continue
|
|
fn += 1
|
|
|
|
if fn % DETECT_EVERY == 0:
|
|
board = read_board(frame, calib)
|
|
hero = read_hero(frame, calib)
|
|
dealer = find_dealer(frame, calib)
|
|
|
|
lb = board_buf.push(board)
|
|
lh = hero_buf.push(hero)
|
|
if lb is not None: state['board'] = lb
|
|
if lh is not None: state['hero'] = lh
|
|
if dealer is not None: state['dealer'] = dealer
|
|
|
|
state['locked'] = bool(state['board'] or
|
|
any('??' not in c for c in state['hero']))
|
|
|
|
hero_valid = [c for c in state['hero'] if '??' not in c]
|
|
if len(hero_valid) == 2:
|
|
eq = calc_equity(hero_valid, state['board'])
|
|
if eq is not None:
|
|
state['equity'] = eq
|
|
state['advice'] = advice(eq)
|
|
|
|
if state['dealer'] is not None:
|
|
state['hero_position'] = position_name(state['dealer'], 0)
|
|
|
|
cv2.imshow(WIN_INFO, draw_info_panel(state))
|
|
|
|
display = cv2.resize(frame, (960, 540))
|
|
sx, sy = 960/1920, 540/1080
|
|
for key, v in calib.items():
|
|
if 'board' in key: col = (0, 200, 0)
|
|
elif 'hero' in key: col = (0, 140, 255)
|
|
elif 'player' in key:col = (180, 0, 180)
|
|
else: continue
|
|
x1,y1 = int(v['x1']*sx), int(v['y1']*sy)
|
|
x2,y2 = int(v['x2']*sx), int(v['y2']*sy)
|
|
cv2.rectangle(display, (x1,y1), (x2,y2), col, 1)
|
|
cv2.putText(display, key.split('_')[-1], (x1+2,y1+11),
|
|
FONT, 0.30, col, 1)
|
|
cv2.imshow(WIN_CAM, display)
|
|
|
|
k = cv2.waitKey(1) & 0xFF
|
|
if k in (27, ord('q')): break
|
|
if k == ord('r'):
|
|
board_buf.reset(); hero_buf.reset()
|
|
state.update({'board':[],'hero':['??','??'],'equity':None,
|
|
'advice':'','locked':False})
|
|
cv2.imshow(WIN_INFO, draw_info_panel(state))
|
|
|
|
try:
|
|
cam_vis = cv2.getWindowProperty(WIN_CAM, cv2.WND_PROP_VISIBLE)
|
|
info_vis = cv2.getWindowProperty(WIN_INFO, cv2.WND_PROP_VISIBLE)
|
|
if cam_vis < 1 and info_vis < 1: break
|
|
except: break
|
|
|
|
cap.release()
|
|
cv2.destroyAllWindows()
|
|
if _tess: _tess.End()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|