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