Бросьте вызов мировому рекорду по разминированию: реальный бой Python+OpenCV с автоматизацией разминирования

задняя часть Python OpenCV

Давайте не будем нести чушь, давайте сначала посмотрим на результаты~

Промежуточный - 0,74 секунды 3БВ/С=60,81

Я полагаю, что многие люди давно знают, что существует такая классическая игра для тестирования видеокарт, как тральщик, и многие слышали о известном имени Го Вэйцзя, первого в мире тральщика. Как классическая игра, родившаяся в эпоху Windows 9x, Minesweeper по-прежнему обладает своим уникальным очарованием, перенесенным из прошлого в настоящее: требования к быстрому и точному управлению мышью, возможности быстрого отклика и острые ощущения от побития рекордов. волнение, принесенное друзьями Лея, принадлежит только тральщику.

▍0x00 готов

Перед подготовкой к созданию комплекта программного обеспечения для автоматизации траления необходимо подготовить следующие инструменты/программное обеспечение/окружение

- Среда разработки

  1. Среда Python3 — рекомендуется версия 3.6 или выше[Более рекомендуется Anaconda3, многие из следующих зависимостей не нужно устанавливать]
  2. Библиотека зависимостей Numpy [нет необходимости устанавливать, если Anaconda доступна]
  3. Библиотека зависимостей PIL [нет необходимости устанавливать, если Anaconda]
  4. opencv-python
  5. win32gui, библиотека зависимостей win32api
  6. IDE с поддержкой Python [необязательно, если вы умеете писать программы с помощью текстового редактора]

- Программное обеспечение сапера

· Minesweeper Arbiter ссылка на скачивание

Конечно, перед официальным стартом нам также необходимо понять основы разминирования (читатели, которые уже разбираются в основах разминирования, могут его сразу пропустить). Здесь автор цитирует крупнейший форум по разминированию в Китае——Saolei.netстатья в .

(Исходный адрес:Сапер-новичок в дороге - Minesweeper Network Saolei.net)

Итак, наши приготовления завершены! Давайте начнем~

▍0x01 Идеи реализации

Что важнее всего перед тем, как что-то делать? Это построение структуры шагов в уме этого дела, которое должно быть сделано. Только так мы можем добиться того, чтобы в процессе этого мы были максимально вдумчивы, чтобы в итоге был хороший результат. При написании программ мы должны изо всех сил стараться иметь в виду общую идею, прежде чем мы официально начнем разработку.

Для этого проекта общий процесс разработки выглядит следующим образом:

  1. Заполните раздел перехвата содержимого формы
  2. Заполните раздел сегментации шахтных блоков
  3. Заполните раздел идентификации типа минного блока
  4. Завершить алгоритм сапера

Хорошо, теперь, когда у нас есть идея, давайте засучим рукава и усердно поработаем!

- 01 Перехват формы

По сути, для этого проекта перехват формы — логически простая, но довольно хлопотная в реализации часть, а также обязательная часть. Через Spy++ автор получил следующие две части информации:

 class_name = "TMain"
   title_name = "Minesweeper Arbiter "
  • Основной класс формы ms_arbiter.exe — «TMain».
  • Основная форма ms_arbiter.exe называется «Арбитр сапера».

Я это заметил? После имени главной формы есть пробелы. Именно это пространство, поэтому я некоторое время беспокоился, просто добавьте это пространство, чтобы win32gui мог получить обычную форму дескриптора.

Этот проект использует win32gui для получения информации о местоположении формы. Конкретный код выглядит следующим образом:

 hwnd = win32gui.FindWindow(class_name, title_name)
   if hwnd:
        left, top, right, bottom = win32gui.GetWindowRect(hwnd)

Через приведенный выше код мы получаем положение формы относительно всего экрана. После этого нам нужно использовать PIL для перехвата шахматной доски интерфейса тральщика.

Сначала нам нужно импортировать библиотеку PIL.

from PIL import ImageGrab

Затем выполните определенные операции.

left += 15
top += 101
right -= 15
bottom -= 43

rect = (left, top, right, bottom)
img = ImageGrab.grab().crop(rect)

Если вы сообразительны, то должны были с первого взгляда найти эти странные Волшебные Числа.Да, это действительно Волшебные Числа.Это положение всей шахматной доски относительно окна, полученное автором путем небольшой корректировки.

Примечание. Эти данные тестируются только в Windows 10. Если они находятся в других системах Windows, правильность относительного положения не гарантируется, поскольку в старых версиях системы границы окон могут быть разной ширины.

Оранжевая область — то, что нам нужно

Хорошо, у нас есть изображение шахматной доски, следующим шагом будет сегментация изображения каждого блока грома~

- 02 Сегментация блока грома

Перед тем, как разделить шахтный блок, нам нужно заранее знать размер шахтного блока и размер его рамы. После авторского замера под ms_arbiter размер каждого блока шахты 16px*16px.

Зная размер шахтного блока, мы можем разрезать каждый шахтный блок. Сначала нам нужно узнать количество минных блоков как по горизонтали, так и по вертикали.

 block_width, block_height = 16, 16
  blocks_x = int((right - left) / block_width)
  blocks_y = int((bottom - top) / block_height)

После этого мы создаем двумерный массив для хранения изображения каждого блока шахты, выполняем сегментацию изображения и сохраняем его в ранее созданный массив.

 def crop_block(hole_img, x, y):
        x1, y1 = x * block_width, y * block_height
        x2, y2 = x1 + block_width, y1 + block_height
        return hole_img.crop((x1, y1, x2, y2))

    blocks_img = [[0 for i in range(blocks_y)] for i in range(blocks_x)]
    
    for y in range(blocks_y):
        for x in range(blocks_x):
            blocks_img[x][y] = crop_block(img, x, y)

Инкапсулировать всю часть получения и сегментации изображения в библиотеку, которую можно вызвать в любое время.В авторской реализации автор инкапсулирует эту часть в imageProcess.py, в котором функция get_frame() используется для завершения вышеуказанного получения изображения и сегментация Процесс.

- 03 идентификация минного блока

Эта часть, вероятно, самая важная часть всего проекта, помимо самого алгоритма траления. Автор использует относительно простую функцию обнаружения минных блоков, которая эффективна и соответствует требованиям.

 def analyze_block(self, block, location):
        block = imageProcess.pil_to_cv(block)

        block_color = block[8, 8]
        x, y = location[0], location[1]

        # -1:Not opened
        # -2:Opened but blank
        # -3:Un initialized

        # Opened
        if self.equal(block_color, self.rgb_to_bgr((192, 192, 192))):
            if not self.equal(block[8, 1], self.rgb_to_bgr((255, 255, 255))):
                self.blocks_num[x][y] = -2
                self.is_started = True
            else:
                self.blocks_num[x][y] = -1

        elif self.equal(block_color, self.rgb_to_bgr((0, 0, 255))):
            self.blocks_num[x][y] = 1

        elif self.equal(block_color, self.rgb_to_bgr((0, 128, 0))):
            self.blocks_num[x][y] = 2

        elif self.equal(block_color, self.rgb_to_bgr((255, 0, 0))):
            self.blocks_num[x][y] = 3

        elif self.equal(block_color, self.rgb_to_bgr((0, 0, 128))):
            self.blocks_num[x][y] = 4

        elif self.equal(block_color, self.rgb_to_bgr((128, 0, 0))):
            self.blocks_num[x][y] = 5

        elif self.equal(block_color, self.rgb_to_bgr((0, 128, 128))):
            self.blocks_num[x][y] = 6

        elif self.equal(block_color, self.rgb_to_bgr((0, 0, 0))):
            if self.equal(block[6, 6], self.rgb_to_bgr((255, 255, 255))):
                # Is mine
                self.blocks_num[x][y] = 9
            elif self.equal(block[5, 8], self.rgb_to_bgr((255, 0, 0))):
                # Is flag
                self.blocks_num[x][y] = 0
            else:
                self.blocks_num[x][y] = 7

        elif self.equal(block_color, self.rgb_to_bgr((128, 128, 128))):
            self.blocks_num[x][y] = 8
        else:
            self.blocks_num[x][y] = -3
            self.is_mine_form = False

        if self.blocks_num[x][y] == -3 or not self.blocks_num[x][y] == -1:
            self.is_new_start = False

Читатели могут видеть, что автор использует метод считывания пикселей центральной точки каждого минного блока, чтобы определить тип минного блока, и дополнительно оценивает ситуацию вставки флага, не нажат, нажат, но пуст и т. д. Конкретное значение цвета получается непосредственно автором, а цвет скриншота не сжимается, поэтому достаточно судить о категории, комбинируя центральный пиксель с другими характерными точками, и это очень эффективно.

В данном проекте автор при реализации использует следующие методы аннотации:

  • 1-8: Представляет числа от 1 до 8
  • 9: указывает на мины
  • 0: указывает флаги
  • -1: означает не открыто
  • -2: означает открытый, но пустой
  • -3: означает не любой тип блока в Minesweeper.

Таким простым, быстрым и эффективным способом мы успешно добились высокоэффективного распознавания изображений.

- 04 Реализация алгоритма тральщика

Это, пожалуй, самая захватывающая часть этой статьи. Здесь автору необходимо сначала объяснить конкретную идею алгоритма траления мин:

  1. Пройдите через каждый шахтный блок, у которого уже есть номер, и определите, совпадает ли количество неоткрытых шахтных блоков в сетке из девяти клеток вокруг него с его собственным номером. сетки - это мины, и пометьте их.
  2. Снова пройдите каждый пронумерованный минный блок, возьмите все минные блоки, которые не были открыты в пределах сетки из девяти квадратов, удалите минные блоки, отмеченные как мины при последнем обходе, запишите и щелкните по ним.
  3. Если вышеуказанный метод не может продолжаться, это означает, что вы столкнулись с мертвым концом и выберите, что случайно нажимают все в данный момент неопущенные шахты. (Конечно, этот метод не является оптимальным, существует более превосходные решения, но реализация относительно хлопотно)

Базовый процесс траления таков, так что давайте реализуем его сами~

Прежде всего, нам нужен метод, который может найти положения всех квадратов в сетке из девяти квадратов шахтного блока. Из-за специфики игры «Сапер» на четырех сторонах шахматной доски нет края сетки из девяти клеток, поэтому нам нужно фильтровать, чтобы исключить доступ, который может выйти за границу.

def generate_kernel(k, k_width, k_height, block_location):

     ls = []
     loc_x, loc_y = block_location[0], block_location[1]

     for now_y in range(k_height):
         for now_x in range(k_width):
             if k[now_y][now_x]:
                 rel_x, rel_y = now_x - 1, now_y - 1
                 ls.append((loc_y + rel_y, loc_x + rel_x))
     return ls

 kernel_width, kernel_height = 3, 3

 # Kernel mode:[Row][Col]
 kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]

 # Left border
 if x == 0:
     for i in range(kernel_height):
         kernel[i][0] = 0

 # Right border
 if x == self.blocks_x - 1:
     for i in range(kernel_height):
         kernel[i][kernel_width - 1] = 0

 # Top border
 if y == 0:
     for i in range(kernel_width):
         kernel[0][i] = 0

 # Bottom border
 if y == self.blocks_y - 1:
     for i in range(kernel_width):
         kernel[kernel_height - 1][i] = 0

 # Generate the search map
 to_visit = generate_kernel(kernel, kernel_width, kernel_height, location)

В этой части автор удаляет ядро, определяя, находится ли текущий блок мины на каждом краю шахматной доски (в ядре 1 зарезервировано, 0 отброшено), а затем окончательные координаты генерируются функцией generate_kernel.

def count_unopen_blocks(blocks):
    count = 0
    for single_block in blocks:
        if self.blocks_num[single_block[1]][single_block[0]] == -1:
            count += 1
    return count

def mark_as_mine(blocks):
    for single_block in blocks:
        if self.blocks_num[single_block[1]][single_block[0]] == -1:
            self.blocks_is_mine[single_block[1]][single_block[0]] = 1 

unopen_blocks = count_unopen_blocks(to_visit)
if unopen_blocks == self.blocks_num[x][y]:
     mark_as_mine(to_visit)

После завершения генерации ядра имеем адресную книгу моего блока, которую нужно обнаружить: to_visit. После этого мы используем функцию count_unopen_blocks для подсчета количества неоткрытых блоков в окружающей сетке из девяти квадратов и сравниваем его с текущим количеством минных блоков.Если они равны, помечаем все шахтные блоки из девяти квадратов как мины через функция mark_as_mine.

def mark_to_click_block(blocks):
   for single_block in blocks:

       # Not Mine
       if not self.blocks_is_mine[single_block[1]][single_block[0]] == 1:
           # Click-able
           if self.blocks_num[single_block[1]][single_block[0]] == -1:

               # Source Syntax: [y][x] - Converted
               if not (single_block[1], single_block[0]) in self.next_steps:
                   self.next_steps.append((single_block[1], single_block[0]))

def count_mines(blocks):
    count = 0
    for single_block in blocks:
        if self.blocks_is_mine[single_block[1]][single_block[0]] == 1:
            count += 1
    return count

mines_count = count_mines(to_visit)

if mines_count == block:
    mark_to_click_block(to_visit)

Во второй части процесса разминирования мы также использовали метод, аналогичный первому этапу. Во-первых, используйте тот же самый метод, что и в первом шаге, чтобы сгенерировать ядро ​​шахтного блока, к которому необходимо получить доступ, затем сгенерируйте конкретное расположение шахтного блока, используйте функцию count_mines, чтобы получить количество всех шахтных блоков в Jiugongge, и судите все мины в текущем Jiugongge были обнаружены.

Если это так, используйте функцию mark_to_click_block, чтобы исключить минные блоки, которые были отмечены как мины в сетке из девяти квадратов, и добавьте оставшиеся безопасные минные блоки в массив next_steps.

# Analyze the number of blocks
 self.iterate_blocks_image(BoomMine.analyze_block)

 # Mark all mines
 self.iterate_blocks_number(BoomMine.detect_mine)

 # Calculate where to click
 self.iterate_blocks_number(BoomMine.detect_to_click_block)

 if self.is_in_form(mouseOperation.get_mouse_point()):
     for to_click in self.next_steps:
         on_screen_location = self.rel_loc_to_real(to_click)
         mouseOperation.mouse_move(on_screen_location[0], on_screen_location[1])
         mouseOperation.mouse_click()

В окончательной реализации автор инкапсулирует несколько процессов в функции, и может использовать входящую функцию для обработки всех моих блоков через метод iterate_blocks_number, который чем-то похож на функцию Filter в Python.

После этого работа, которую я делаю, заключается в том, чтобы определить, находится ли текущая позиция мыши в пределах шахматной доски.Если это так, она автоматически начнет распознавать и щелкать. Для конкретной части щелчка автор использует код (собранный из Интернета), написанный автором как «wp», который реализует работу отправки сообщения формы на основе win32api, а затем завершает операцию движения мыши и щелчка. Конкретная реализация инкапсулирована в mouseOperation.py, и если вам интересно, вы можете просмотреть ее в репозитории Github в конце статьи.

запись автора

Этот результат, даже первый в мире Го Вэйцзя должен дрожать!

Автор также реализовал функцию случайного нажатия, чтобы открыть ситуацию при запуске новой игры, но поскольку это относительно просто, подробный анализ здесь размещаться не будет~

Примечание: Если вы обнаружите, что во время вашего эксперимента будет взорван громовой блок, не волнуйтесь, это потому, что вы уже столкнулись с тупиком, и вы не можете делать прямые выводы с помощью алгоритма этого проекта. , программа будет нажиматься случайным образом, и есть определенный шанс лопнуть!

  • Полный код проекта/адрес GitHub |ArtrixTech/BoomMine
  • Автор / Автор этой статьи | Arttrix

Спасибо за ваше терпение в чтении ~ Если вам это нравится, ставьте лайк, собирайте и награждайте!