Не так давно я увлекся покером, и, поскольку мне нравилось работать с компьютерным зрением, я решил совместить приятное с полезным.

Общее функционирование программы

Сразу отмечу, что в качестве игрового зала я выбрал PokerStars и самую популярную разновидность покера — техасский холдем. Программа запускает бесконечный цикл, который считывает определенную область экрана, где находится покерный стол. Когда приходит наш (герой) ход, всплывает или обновляется окно со следующей информацией:

  • какие карты мы сейчас держим
  • какие карты сейчас на столе
  • общий банк
  • капитал
  • позиция и ставка каждого игрока

Визуально это выглядит следующим образом:

Определение движения героя

Прямо под карточками героя есть небольшая область, которая может быть как черной, так и серой:

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

res_img = self.img[self.cfg['hero_step_define']['y_0']:self.cfg['hero_step_define']['y_1'],
                           self.cfg['hero_step_define']['x_0']:self.cfg['hero_step_define']['x_1']]

hsv_img = cv2.cvtColor(res_img, cv2.COLOR_BGR2HSV_FULL)
mask = cv2.inRange(hsv_img, np.array(self.cfg['hero_step_define']['lower_gray_color']),
                            np.array(self.cfg['hero_step_define']['upper_gray_color']))
count_of_white_pixels = cv2.countNonZero(mask)

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

мы получаем следующее бинарное изображение:

После этого мы находим внешние контуры значений и мастей с помощью функции findContours(), которые затем передаем в функцию boundingRect(), которая возвращает ограничивающие рамки каждого контура. Хорошо, теперь у нас есть коробки со всеми картами, но как мы узнаем, есть ли у нас, например, туз червей? Для этого я нашел и вручную обрезал каждое значение и каждую масть и поместил эти изображения в специальную папку в качестве эталонных изображений. Затем мы вычисляем MSE между каждым из эталонных изображений и обрезанными изображениями карточек с помощью этого кода:

err = np.sum((img.astype("float") - benchmark_img.astype("float")) ** 2)
err /= float(img.shape[0] * img.shape[1])

Мы присваиваем эталонному изображению с наименьшим именем ошибки ящику. Вполне легко :)

Определение банка и ставки игрока. Как найти кнопку дилера

Для определения банка будем работать с шаблонным изображением этого вида:

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

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

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

Если вам нужно действовать первым, вы находитесь в ранней позиции. Если вы находитесь в поздней позиции, ваша очередь действовать одной из последних. За столом 6-макс позиции следующие:

Чтобы определить, кто является дилером, мы также берем изображение шаблона, как вы можете видеть:

Находим координаты левого верхнего угла изображения на таблице и используем формулу расстояния между двумя точками на плоскости. Прописываем в конфигурационном файле вторые координаты x и y (координаты центра игрока), чтобы определить, кто находится ближе к кнопке, и они будут ее владельцем :).

Распознавание вакантных мест и отсутствующих игроков

Часто бывает так, что за столом сидят пять игроков вместо шести, поэтому свободное место помечается таким образом:

Под ником отсутствующего в данный момент игрока появляется следующая надпись:

Для обнаружения наличия таких игроков мы вводим эти изображения и изображение стола в качестве шаблонов и снова передаем их функции matchTemplate(). На этот раз мы возвращаем не координаты, а вероятность того, насколько похожи два изображения. Если вероятность между первым изображением и столом высока, у нас есть стол, в котором отсутствует игрок.

Расчет капитала

Эквити — это вероятность выигрыша конкретной руки против двух конкретных карт или диапазона оппонента. Математически эквити рассчитывается как отношение возможных выигрышных комбинаций к общему количеству возможных комбинаций. На Python этот алгоритм можно реализовать с помощью библиотеки eval7 (которая в данном случае помогает оценить, насколько сильна рука). Это выглядит следующим образом:

deck = [eval7.Card(card) for card in deck]
table_cards = [eval7.Card(card) for card in table_cards]
hero_cards = [eval7.Card(card) for card in hero_cards]
max_table_cards = 5
win_count = 0
for _ in range(iters):
    np.random.shuffle(deck)
    num_remaining = max_table_cards - len(table_cards)
    draw = deck[:num_remaining+2]
    opp_hole, remaining_comm = draw[:2], draw[2:]
    player_hand = hero_cards + table_cards + remaining_comm
    opp_hand = opp_hole + table_cards + remaining_comm
    player_strength = eval7.evaluate(player_hand)
    opp_strength = eval7.evaluate(opp_hand)

    if player_strength > opp_strength:
        win_count += 1

win_prob = (win_count / iters) * 100

Заключение

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

Если кто хочет поучаствовать в проекте или есть идеи по его развитию — пишите! Исходный код, как всегда, доступен на GitHub.

Всем хорошего дня!