Как стать автором
Обновить

Использование компьютерного зрения для игры в покер

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров12K

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

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

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

  1. какие карты сейчас у нас на руках;

  2. карты, которые сейчас на столе;

  3. общий банк;

  4. эквити;

  5. о позиции и о ставке каждого игрока.

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

Определение хода героя

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

Если данная область горит серым — ход наш, в противном случае — ход соперника. Так как у нас изображение статично, то мы вырезаем по координатам данную область и работаем с ней, а далее с помощью функции 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(), которая возвращает ограничительные рамки каждого контура. Чудненько, теперь у нас есть боксы всех карт, но как нам понять, что у нас на карте, к примеру, туз червей? Для этого я нашёл и обрезал вручную каждое значение и каждую масть, и поместил данные изображения в специальную папку как эталонные изображения. После этого считаем среднеквадратическую ошибку между каждым эталонным и обрезанной карты изображениями:

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

Для какого эталонного изображения ошибка получилась меньше всего, то и имя изображения присваиваем боксу. Всё просто:)

Определение банка и ставки игрока. Нахождение фишки дилера

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

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

Фишка дилера в покере — обязательный атрибут, определяет очерёдность действий и торга всех участников игры. Если вы должны действовать одним из первых, то вы находитесь в ранней позиции. Если вы сидите в поздней позиции, то ваша очередь хода наступает одной из последних. Для 6-max стола, а мы именно такой и рассматриваем, позиции распределяются следующим образом:

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

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

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

Так часто бывает, что за столом вместо 6 игроков сидит 5, тогда свободное место помечается подобным образом:

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

Для выявления присутствия таких игроков, берём данные изображения в качестве шаблонных и изображение стола, и опять же подаём на вход функции 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. Всем хорошего дня!

Теги:
Хабы:
Всего голосов 8: ↑8 и ↓0+8
Комментарии32

Публикации

Истории

Работа

Python разработчик
137 вакансий
Data Scientist
61 вакансия

Ближайшие события