208 lines
7.1 KiB
Python
208 lines
7.1 KiB
Python
"""
|
|
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()
|