Загрузка данных


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