CheckerChesser / src /vision.py
algorembrant's picture
Upload 18 files
57f1366 verified
import cv2
import numpy as np
import mss
class VisionHandler:
def __init__(self):
self.templates = {}
self.is_calibrated = False
def capture_screen(self, region):
"""
Capture a specific region of the screen.
region: {'top': int, 'left': int, 'width': int, 'height': int}
"""
with mss.mss() as sct:
# mss requires int for all values
monitor = {
"top": int(region["top"]),
"left": int(region["left"]),
"width": int(region["width"]),
"height": int(region["height"])
}
screenshot = sct.grab(monitor)
img = np.array(screenshot)
return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
def split_board(self, board_image):
"""
Split board image into 64 squares.
Assumption: board_image is cropped exactly to the board edges.
"""
h, w, _ = board_image.shape
sq_h, sq_w = h // 8, w // 8
squares = []
# Row-major order (rank 8 down to rank 1)
# Standard FEN order: Rank 8 (index 0) to Rank 1 (index 7)
for r in range(8):
row_squares = []
for c in range(8):
# Add a small crop margin to avoid border noise
margin_h = int(sq_h * 0.1)
margin_w = int(sq_w * 0.1)
y1 = r * sq_h + margin_h
y2 = (r + 1) * sq_h - margin_h
x1 = c * sq_w + margin_w
x2 = (c + 1) * sq_w - margin_w
square = board_image[y1:y2, x1:x2]
row_squares.append(square)
squares.append(row_squares)
return squares
def calibrate(self, board_image):
"""
Calibrate by learning piece appearance from a starting position board image.
Standard Starting Position:
r n b q k b n r
p p p p p p p p
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
P P P P P P P P
R N B Q K B N R
"""
squares = self.split_board(board_image)
self.templates = {}
# Dictionary mapping piece chars to list of coordinates (row, col) in start pos
# We'll take the average or just the first instance as template
# 0-indexed rows: 0=BlackBack, 1=BlackPawn, ... 6=WhitePawn, 7=WhiteBack
piece_map = {
'r': [(0, 0), (0, 7)],
'n': [(0, 1), (0, 6)],
'b': [(0, 2), (0, 5)],
'q': [(0, 3)],
'k': [(0, 4)],
'p': [(1, i) for i in range(8)],
'R': [(7, 0), (7, 7)],
'N': [(7, 1), (7, 6)],
'B': [(7, 2), (7, 5)],
'Q': [(7, 3)],
'K': [(7, 4)],
'P': [(6, i) for i in range(8)],
'.': [] # Empty squares
}
# Add empty squares (rows 2, 3, 4, 5)
for r in range(2, 6):
for c in range(8):
piece_map['.'].append((r, c))
# Store one template per piece type (simplest approach)
# Better: Store all samples and use KNN, but simple template match might suffice for consistent graphics
for p_char, coords in piece_map.items():
if not coords:
continue
# Use the first coordinate as the primary template
r, c = coords[0]
template = squares[r][c]
# We convert to grayscale for simpler matching
gray_template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
# Store just one template for now
self.templates[p_char] = gray_template
self.is_calibrated = True
print("Calibration complete.")
def match_square(self, square_img):
if not self.templates:
return '.'
gray_sq = cv2.cvtColor(square_img, cv2.COLOR_BGR2GRAY)
best_score = float('inf')
best_piece = '.'
# Compare against all templates using MSE
# Note: Templates must be same size. If slightly different due to rounding, resize.
target_h, target_w = gray_sq.shape
for p_char, template in self.templates.items():
# Resize template to match square if needed (should be identical if grid is uniform)
if template.shape != gray_sq.shape:
template = cv2.resize(template, (target_w, target_h))
# Mean Squared Error
err = np.sum((gray_sq.astype("float") - template.astype("float")) ** 2)
err /= float(gray_sq.shape[0] * gray_sq.shape[1])
if err < best_score:
best_score = err
best_piece = p_char
return best_piece
def get_fen_from_image(self, board_image):
if not self.is_calibrated:
# Fallback or error
return None
squares = self.split_board(board_image)
fen_rows = []
for r in range(8):
empty_count = 0
row_str = ""
for c in range(8):
piece = self.match_square(squares[r][c])
if piece == '.':
empty_count += 1
else:
if empty_count > 0:
row_str += str(empty_count)
empty_count = 0
row_str += piece
if empty_count > 0:
row_str += str(empty_count)
fen_rows.append(row_str)
# Join with '/'
fen_board = "/".join(fen_rows)
# Add default game state info (w KQkq - 0 1)
# TODO: Detect turn based on external logic or diff?
# For now, we might alternate or assume it's always the user's turn to move?
# Actually, if we are just showing analysis, we want the engine to evaluate for the SIDE TO MOVE.
# But we don't know who is to move just from the static board.
# We can try to infer from previous state or just ask the user.
# For this version, let's return the board part, and handle the rest in logic.
return f"{fen_board} w KQkq - 0 1"