""" Manual calibration: drag and resize labeled rectangles, then press S to save. """ import cv2 import json CALIB_FILE = '/home/isaevea/python/pp/calibration.json' FONT = cv2.FONT_HERSHEY_SIMPLEX HANDLE = 10 # corner handle size in px # Initial rectangles [name, label, x1,y1,x2,y2, color_BGR] # Positions are in 1280x720 display space DEFAULTS = [ # Board cards ['board_card_1', 'Board 1', 230, 240, 285, 320, (0, 200, 0)], ['board_card_2', 'Board 2', 295, 240, 350, 320, (0, 200, 0)], ['board_card_3', 'Board 3', 360, 240, 415, 320, (0, 200, 0)], ['board_card_4', 'Board 4', 425, 240, 480, 320, (0, 200, 0)], ['board_card_5', 'Board 5', 490, 240, 545, 320, (0, 200, 0)], # Hero cards ['hero_card_1', 'Hero 1', 395, 440, 450, 510, (255, 140, 0)], ['hero_card_2', 'Hero 2', 460, 440, 515, 510, (255, 140, 0)], # Player dealer zones — расставь по позициям игроков вокруг стола ['player_0', 'P0 Hero', 370, 490, 450, 540, (180, 0, 180)], ['player_1', 'P1', 170, 410, 250, 460, (180, 0, 180)], ['player_2', 'P2', 80, 270, 160, 320, (180, 0, 180)], ['player_3', 'P3', 175, 120, 255, 170, (180, 0, 180)], ['player_4', 'P4', 370, 80, 450, 130, (180, 0, 180)], ['player_5', 'P5', 570, 120, 650, 170, (180, 0, 180)], ['player_6', 'P6', 660, 270, 740, 320, (180, 0, 180)], ['player_7', 'P7', 570, 410, 650, 460, (180, 0, 180)], ] W, H = 1280, 720 # display size def load_existing(): try: with open(CALIB_FILE) as f: data = json.load(f) # scale stored frame coords (1920x1080) → display coords (1280x720) sx, sy = W/1920, H/1080 for row in DEFAULTS: key = row[0] if key in data: v = data[key] row[2] = int(v['x1']*sx) row[3] = int(v['y1']*sy) row[4] = int(v['x2']*sx) row[5] = int(v['y2']*sy) except Exception: pass def save(rects): sx, sy = 1920/W, 1080/H data = {} for name, label, x1,y1,x2,y2, col in rects: data[name] = { 'x1': int(x1*sx), 'y1': int(y1*sy), 'x2': int(x2*sx), 'y2': int(y2*sy), } with open(CALIB_FILE, 'w') as f: json.dump(data, f, indent=2) print(f'Saved to {CALIB_FILE}') # ── Mouse state ─────────────────────────────────────────────────────────────── sel = None # index of selected rect mode = None # 'move' | 'TL'|'TR'|'BL'|'BR'|'T'|'B'|'L'|'R' mx0 = my0 = 0 rx0 = ry0 = rx1 = ry1 = 0 def hit_handle(x, y, rx1_, ry1_, rx2_, ry2_): """Return corner/edge name if (x,y) is near a handle, else None.""" cx = (rx1_+rx2_)//2; cy = (ry1_+ry2_)//2 checks = [ ('TL', rx1_, ry1_), ('TR', rx2_, ry1_), ('BL', rx1_, ry2_), ('BR', rx2_, ry2_), ('T', cx, ry1_), ('B', cx, ry2_), ('L', rx1_, cy ), ('R', rx2_, cy ), ] for name, hx, hy in checks: if abs(x-hx) <= HANDLE and abs(y-hy) <= HANDLE: return name return None def mouse(event, x, y, flags, rects): global sel, mode, mx0, my0, rx0, ry0, rx1, ry1 if event == cv2.EVENT_LBUTTONDOWN: # check handles first, then interior for i, (name, label, x1,y1,x2,y2, col) in enumerate(rects): h = hit_handle(x, y, x1,y1,x2,y2) if h: sel=i; mode=h; mx0=x; my0=y rx0,ry0,rx1,ry1 = x1,y1,x2,y2 return for i, (name, label, x1,y1,x2,y2, col) in enumerate(rects): if x1<=x<=x2 and y1<=y<=y2: sel=i; mode='move'; mx0=x; my0=y rx0,ry0,rx1,ry1 = x1,y1,x2,y2 return sel=None elif event == cv2.EVENT_MOUSEMOVE and sel is not None and (flags & cv2.EVENT_FLAG_LBUTTON): dx=x-mx0; dy=y-my0 r = rects[sel] if mode=='move': w=rx1-rx0; h2=ry1-ry0 r[2]=max(0,rx0+dx); r[3]=max(0,ry0+dy) r[4]=r[2]+w; r[5]=r[3]+h2 elif mode=='TL': r[2]=min(rx0+dx,rx1-5); r[3]=min(ry0+dy,ry1-5) elif mode=='TR': r[4]=max(rx1+dx,rx0+5); r[3]=min(ry0+dy,ry1-5) elif mode=='BL': r[2]=min(rx0+dx,rx1-5); r[5]=max(ry1+dy,ry0+5) elif mode=='BR': r[4]=max(rx1+dx,rx0+5); r[5]=max(ry1+dy,ry0+5) elif mode=='T': r[3]=min(ry0+dy,ry1-5) elif mode=='B': r[5]=max(ry1+dy,ry0+5) elif mode=='L': r[2]=min(rx0+dx,rx1-5) elif mode=='R': r[4]=max(rx1+dx,rx0+5) elif event == cv2.EVENT_LBUTTONUP: mode=None def draw_rect(img, row, is_sel): name, label, x1,y1,x2,y2, col = row thick = 3 if is_sel else 2 border = (255,255,255) if is_sel else col cv2.rectangle(img, (x1,y1), (x2,y2), border, thick) # label background (tw,th),_ = cv2.getTextSize(label, FONT, 0.45, 1) cv2.rectangle(img, (x1,y1-th-4), (x1+tw+4,y1), col, -1) cv2.putText(img, label, (x1+2,y1-3), FONT, 0.45, (0,0,0), 1) if is_sel: # draw handles cx=(x1+x2)//2; cy=(y1+y2)//2 for hx,hy in [(x1,y1),(x2,y1),(x1,y2),(x2,y2), (cx,y1),(cx,y2),(x1,cy),(x2,cy)]: cv2.rectangle(img,(hx-HANDLE,hy-HANDLE),(hx+HANDLE,hy+HANDLE),(255,255,255),-1) cv2.rectangle(img,(hx-HANDLE,hy-HANDLE),(hx+HANDLE,hy+HANDLE),(0,0,0),1) def calibrate(): load_existing() rects = [list(r) for r in DEFAULTS] 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() WIN = 'Calibration | S=save R=reset Q=quit' cv2.namedWindow(WIN, cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL) cv2.resizeWindow(WIN, W, H) cv2.setMouseCallback(WIN, lambda e,x,y,f,p: mouse(e,x,y,f,rects)) # init window ret, frame = cap.read() if ret: cv2.imshow(WIN, cv2.resize(frame, (W,H))) cv2.waitKey(1) while True: ret, frame = cap.read() if not ret: break display = cv2.resize(frame, (W, H)) # dim overlay ov = display.copy() cv2.rectangle(ov, (0,0),(W,30),(20,20,20),-1) cv2.addWeighted(ov,0.7,display,0.3,0,display) cv2.putText(display,'S=save R=reset positions Q=quit', (10,20),FONT,0.50,(180,180,180),1) for i,row in enumerate(rects): draw_rect(display, row, i==sel) cv2.imshow(WIN, display) k = cv2.waitKey(16) & 0xFF if k == ord('q'): break if k == ord('s'): save(rects) cv2.putText(display,'SAVED!',(W//2-40,H//2),FONT,1.2,(0,255,0),3) cv2.imshow(WIN, display); cv2.waitKey(800) if k == ord('r'): for i,row in enumerate(rects): for j,v in enumerate(DEFAULTS[i][2:6], 2): row[j] = v try: if cv2.getWindowProperty(WIN, cv2.WND_PROP_VISIBLE) < 1: break except: break cap.release() cv2.destroyAllWindows() if __name__ == '__main__': calibrate()