Загрузка данных
import time
from dataclasses import dataclass
from typing import Optional, Tuple, Dict, Any
import cv2
import mss
import numpy as np
@dataclass
class BoardInfo:
image: np.ndarray
rect: Tuple[int, int, int] # (x, y, size) относительно monitor
cell_size: float
grid_score: float
timestamp: float
gray: np.ndarray
hsv: np.ndarray
edges: np.ndarray
def verify_chessboard_grid(gray_crop):
"""
Проверяет наличие линии сетки 8x8 внутри области.
Возвращает:
(is_grid: bool, score: float)
"""
if gray_crop is None or not hasattr(gray_crop, "shape"):
return False, 0.0
if len(gray_crop.shape) != 2:
return False, 0.0
h, w = gray_crop.shape
if h < 64 or w < 64:
return False, 0.0
# Доска почти квадратная. Для внутренней проверки допустим небольшой люфт.
if abs(h - w) > max(8, int(min(h, w) * 0.08)):
return False, 0.0
# Слегка сгладим шум
blurred = cv2.GaussianBlur(gray_crop, (5, 5), 0)
# Границы
edges = cv2.Canny(blurred, 30, 120)
# Суммы по столбцам и строкам
col_sums = np.sum(edges, axis=0).astype(np.float32)
row_sums = np.sum(edges, axis=1).astype(np.float32)
cell_size = min(w, h) / 8.0
score_x = 0.0
score_y = 0.0
# Ищем линии возле предполагаемых границ клеток
for i in range(1, 8):
idx = int(round(i * cell_size))
x_min = max(0, idx - 2)
x_max = min(w, idx + 3)
y_min = max(0, idx - 2)
y_max = min(h, idx + 3)
if x_max > x_min:
score_x += float(np.max(col_sums[x_min:x_max]))
if y_max > y_min:
score_y += float(np.max(row_sums[y_min:y_max]))
total_score = score_x + score_y
# Дополнительная маленькая проверка: доска должна давать хоть какой-то контраст
edge_density = float(np.count_nonzero(edges)) / float(edges.size)
if edge_density < 0.01:
return False, 0.0
return True, total_score
class BoardTracker:
def __init__(self):
self.sct = mss.mss()
# Главный монитор
self.monitor = self.sct.monitors[1]
# Координаты последней найденной доски: (x, y, size) относительно monitor
self.last_board_rect: Optional[Tuple[int, int, int]] = None
# Время последней проверки
self.last_check_time: float = 0.0
# Последний найденный score
self.last_grid_score: float = 0.0
# Полный кэш информации о доске
self.last_board_info: Optional[BoardInfo] = None
# Параметры поиска
self.min_board_size = 80
self.passive_interval_default = 1.5
def capture_screen(self, region=None):
"""Делает скриншот всего экрана или указанной области."""
target_region = region if region else self.monitor
sct_img = self.sct.grab(target_region)
# BGRA -> BGR
return np.array(sct_img)[:, :, :3]
def _build_board_info(self, board_img: np.ndarray, rect: Tuple[int, int, int], grid_score: float) -> BoardInfo:
gray = cv2.cvtColor(board_img, cv2.COLOR_BGR2GRAY)
hsv = cv2.cvtColor(board_img, cv2.COLOR_BGR2HSV)
edges = cv2.Canny(gray, 30, 120)
cell_size = rect[2] / 8.0
return BoardInfo(
image=board_img,
rect=rect,
cell_size=cell_size,
grid_score=grid_score,
timestamp=time.time(),
gray=gray,
hsv=hsv,
edges=edges,
)
def _screen_rect_from_board_rect(self, board_rect: Tuple[int, int, int]) -> Dict[str, int]:
bx, by, size = board_rect
return {
"top": self.monitor["top"] + by,
"left": self.monitor["left"] + bx,
"width": size,
"height": size,
}
def _validate_cached_board(self, passive_interval: float) -> Optional[BoardInfo]:
"""
Проверяет, что доска по кэшу ещё существует.
Если интервал ещё не прошёл — возвращает картинку без перепроверки.
"""
if self.last_board_rect is None:
return None
now = time.time()
roi = self._screen_rect_from_board_rect(self.last_board_rect)
# Быстрый режим: просто забираем кадр из известной области
if now - self.last_check_time < passive_interval:
board_img = self.capture_screen(roi)
info = self._build_board_info(board_img, self.last_board_rect, self.last_grid_score)
self.last_board_info = info
return info
# Периодическая перепроверка
self.last_check_time = now
board_img = self.capture_screen(roi)
gray = cv2.cvtColor(board_img, cv2.COLOR_BGR2GRAY)
is_grid, score = verify_chessboard_grid(gray)
if is_grid and score > 0:
self.last_grid_score = score
info = self._build_board_info(board_img, self.last_board_rect, score)
self.last_board_info = info
return info
# Доска пропала / закрыта / распознана неверно
self.last_board_rect = None
self.last_board_info = None
self.last_grid_score = 0.0
return None
def _scan_full_screen_for_board(self) -> Optional[BoardInfo]:
"""Полный поиск доски по всему экрану."""
full_img = self.capture_screen()
gray = cv2.cvtColor(full_img, cv2.COLOR_BGR2GRAY)
# Лёгкое сглаживание перед поиском контуров
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 30, 150)
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
best_board = None
best_score = -1.0
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
if w < self.min_board_size or h < self.min_board_size:
continue
# Почти квадрат
aspect_ratio = float(w) / float(h)
if not (0.95 <= aspect_ratio <= 1.05):
continue
size = min(w, h)
crop_gray = gray[y:y + size, x:x + size]
if crop_gray.size == 0:
continue
is_grid, grid_score = verify_chessboard_grid(crop_gray)
if not is_grid:
continue
# Немного предпочитаем большие кандидаты, но не слишком сильно
size_bonus = float(size) * 0.15
total_score = float(grid_score) + size_bonus
if total_score > best_score:
best_score = total_score
best_board = (x, y, size)
if best_board is None:
return None
bx, by, size = best_board
board_img = full_img[by:by + size, bx:bx + size]
# Пересчитаем score уже на точном crop
board_gray = cv2.cvtColor(board_img, cv2.COLOR_BGR2GRAY)
is_grid, grid_score = verify_chessboard_grid(board_gray)
if not is_grid:
return None
self.last_board_rect = best_board
self.last_check_time = time.time()
self.last_grid_score = grid_score
info = self._build_board_info(board_img, best_board, grid_score)
self.last_board_info = info
return info
def get_board_info(self, passive_interval: float = 1.5) -> Optional[Dict[str, Any]]:
"""
Возвращает подробную информацию о найденной доске:
{
"image": BGR board image,
"rect": (x, y, size),
"cell_size": float,
"grid_score": float,
"timestamp": float,
"gray": gray image,
"hsv": hsv image,
"edges": edges image
}
"""
info = self._validate_cached_board(passive_interval)
if info is None:
info = self._scan_full_screen_for_board()
if info is None:
return None
return {
"image": info.image,
"rect": info.rect,
"cell_size": info.cell_size,
"grid_score": info.grid_score,
"timestamp": info.timestamp,
"gray": info.gray,
"hsv": info.hsv,
"edges": info.edges,
}
def get_board_image(self, passive_interval: float = 1.5):
"""
Возвращает только BGR-изображение доски.
Это старый интерфейс, чтобы не ломать analysis.py и logic.py.
"""
info = self.get_board_info(passive_interval=passive_interval)
if info is None:
return None
return info["image"]
# Глобальный экземпляр трекера
_tracker = BoardTracker()
def get_board_image():
"""Главная функция для вызова из logic.py"""
return _tracker.get_board_image()
def get_board_info():
"""Расширенный интерфейс: возвращает картинку и метаданные доски."""
return _tracker.get_board_info()
def find_chessboard_strictly_1to1(input_path=None):
"""
Обёртка для обратной совместимости:
возвращает (None, cropped_board)
"""
board_img = _tracker.get_board_image()
return None, board_img