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


import os
import cv2
import numpy as np

def calculate_combined_hash(image, hash_size=8):
    """
    Вычисляет комбинацию dHash (градиенты) и aHash (средняя форма)
    с предварительной нормализацией контраста.
    """
    # 1. Приводим к оттенкам серого
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
    
    # 2. Выравниваем контраст (CLAHE), чтобы скрины разных тем/разрешений выглядели одинаково
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4, 4))
    enhanced = clahe.apply(gray)
    
    # --- dHash (Difference Hash) ---
    resized_d = cv2.resize(enhanced, (hash_size + 1, hash_size))
    dhash = (resized_d[:, 1:] > resized_d[:, :-1]).flatten()

    # --- aHash (Average Hash) ---
    resized_a = cv2.resize(enhanced, (hash_size, hash_size))
    avg = resized_a.mean()
    ahash = (resized_a > avg).flatten()

    # Объединяем битовые массивы (всего 128 бит)
    return np.concatenate([dhash, ahash])


def hamming_distance(hash1, hash2):
    """Считает разницу бит между двумя объединенными хэшами."""
    return np.count_nonzero(hash1 != hash2)


def get_top_crop(cell_img):
    """Вырезает верхнюю часть клетки (голову фигуры)."""
    h, w = cell_img.shape[:2]
    return cell_img[int(h * 0.05):int(h * 0.70), int(w * 0.15):int(w * 0.85)]


def is_empty_cell(cell_gray, std_threshold=12.0):
    """Проверка на пустую клетку по разбросу пикселей."""
    h, w = cell_gray.shape
    center = cell_gray[int(h * 0.25):int(h * 0.75), int(w * 0.25):int(w * 0.75)]
    return np.std(center) < std_threshold


def load_templates(templates_folder="extracted_pieces"):
    symbol_map = {
        "white_K": "K", "white_Q": "Q", "white_R": "R", "white_B": "B", "white_N": "N", "white_P": "P",
        "black_k": "k", "black_q": "q", "black_r": "r", "black_b": "b", "black_n": "n", "black_p": "p",
    }

    templates = {}
    if not os.path.exists(templates_folder):
        print(f"[!] Ошибка: Папка '{templates_folder}' не найдена!")
        return templates

    for filename in os.listdir(templates_folder):
        if filename.endswith(".png"):
            name_without_ext = os.path.splitext(filename)[0]
            symbol = symbol_map.get(name_without_ext)
            if not symbol:
                continue

            file_path = os.path.join(templates_folder, filename)
            tpl_img = cv2.imread(file_path)
            
            if tpl_img is not None:
                top_part = get_top_crop(tpl_img)
                templates[symbol] = calculate_combined_hash(top_part)

    return templates


def generate_fen(board_matrix):
    fen_rows = []
    for row in board_matrix:
        empty_count = 0
        row_str = ""
        for cell in row:
            if cell == ".":
                empty_count += 1
            else:
                if empty_count > 0:
                    row_str += str(empty_count)
                    empty_count = 0
                row_str += cell
        if empty_count > 0:
            row_str += str(empty_count)
        fen_rows.append(row_str)

    return "/".join(fen_rows) + " w - - 0 1"


def scan_board(
    board_path="board.png",
    templates_folder="extracted_pieces",
    empty_std_thresh=12.0,
    max_hash_diff=45  # Порог увеличьте до ~40-45 (так как размер хэша вырос до 128 бит)
):
    templates = load_templates(templates_folder)
    if not templates:
        print("[!] Ошибка: Нет шаблонов для сравнения.")
        return

    board_img = cv2.imread(board_path)
    if board_img is None:
        print(f"[!] Ошибка: Файл '{board_path}' не найден.")
        return

    img_h, img_w, _ = board_img.shape
    cell_w = img_w / 8.0
    cell_h = img_h / 8.0

    board_matrix = []

    print("Сканирование доски двойным хэшем (dHash + aHash + CLAHE)...\n")

    for row in range(8):
        row_symbols = []
        for col in range(8):
            x1 = int(round(col * cell_w))
            y1 = int(round(row * cell_h))
            x2 = int(round((col + 1) * cell_w))
            y2 = int(round((row + 1) * cell_h))

            cell_crop = board_img[y1:y2, x1:x2]
            cell_gray = cv2.cvtColor(cell_crop, cv2.COLOR_BGR2GRAY)

            # 1. Проверяем, пустая ли клетка
            if is_empty_cell(cell_gray, std_threshold=empty_std_thresh):
                row_symbols.append(".")
            else:
                # 2. Вычисляем устойчивый хэш верхушки
                cell_top = get_top_crop(cell_crop)
                cell_hash = calculate_combined_hash(cell_top)

                best_symbol = "."
                min_dist = float("inf")

                for symbol, tpl_hash in templates.items():
                    dist = hamming_distance(cell_hash, tpl_hash)
                    if dist < min_dist:
                        min_dist = dist
                        best_symbol = symbol

                if min_dist <= max_hash_diff:
                    row_symbols.append(best_symbol)
                else:
                    row_symbols.append(".")

        board_matrix.append(row_symbols)

    # Отрисовка в консоли
    divider = "  +" + "---+" * 8
    print(divider)
    for i, row in enumerate(board_matrix):
        rank_label = 8 - i
        row_content = " | ".join(row)
        print(f"{rank_label} | {row_content} |")
        print(divider)
    print("    a   b   c   d   e   f   g   h\n")

    # Генерация FEN
    fen_string = generate_fen(board_matrix)
    print(f"FEN: {fen_string}\n")


if __name__ == "__main__":
    try:
        scan_board()
    except Exception as e:
        print(f"\n[!] Ошибка: {e}")
    
    input("\nНажмите Enter, чтобы закрыть программу...")