Skip to content

Commit

Permalink
The function to recognize the cards on the table with the NN is added…
Browse files Browse the repository at this point in the history
…, it is left disabled for the general user, for collaborators the option must only be enabled in "QT designer".

Parameters for training the NN are modified since it will be used to recognize the hand and the table.
Simple conflicts are resolved in the TEST.
---.---
The result of the NN on the table was verified with an efficiency of 99.98%, the recognition is done by cutting the area of cards on the table by 10 and processing that image (eliminating cloth and other colors) to result in 0/3/4/5 cards corresponding to the game states, I only configured this in GGpoker, that part needs to be improved.
  • Loading branch information
v3rchi3l committed Jan 29, 2024
1 parent 7b72982 commit 7228415
Show file tree
Hide file tree
Showing 7 changed files with 1,933 additions and 1,740 deletions.
3,451 changes: 1,741 additions & 1,710 deletions poker/gui/ui/table_setup_form.ui

Large diffs are not rendered by default.

97 changes: 92 additions & 5 deletions poker/scraper/table_scraper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Recognize table"""
import logging
from PIL import Image
import numpy as np

from poker.scraper.table_scraper_nn import predict
from poker.scraper.table_setup_actions_and_signals import CARD_SUITES, CARD_VALUES
Expand Down Expand Up @@ -110,18 +112,103 @@ def get_my_cards_nn(self):
def get_table_cards2(self):
"""Get the cards on the table"""
self.table_cards = []
for value in CARD_VALUES:
for suit in CARD_SUITES:
if is_template_in_search_area(self.table_dict, self.screenshot,
value.lower() + suit.lower(), 'table_cards_area'):
self.table_cards.append(value + suit)
if 'use_neural_network_table' in self.table_dict and (
self.table_dict['use_neural_network_table'] == '2' or self.table_dict[
'use_neural_network_table'] == 'CheckState.Checked'):
self.get_table_cards_nn()
return True
else:
for value in CARD_VALUES:
for suit in CARD_SUITES:
if is_template_in_search_area(self.table_dict, self.screenshot,
value.lower() + suit.lower(), 'table_cards_area'):
self.table_cards.append(value + suit)
log.info(f"Table cards: {self.table_cards}")
if len(self.table_cards) == 1 or len(self.table_cards) == 2:
log.warning(f"Only recognized {len(self.table_cards)} cards on the table. "
f"This can happen if cards are sliding in or if some of the templates are wrong")
return False
return True

def get_table_cards_nn(self):
# Sacamos una foto de la zona de las cartas en la mesa
nn_image_area = self.table_dict['table_cards_area']
nn_image = self.screenshot.crop(
(nn_image_area['x1'], nn_image_area['y1'], nn_image_area['x2'], nn_image_area['y2']))
nn_image_rgb = nn_image.convert('RGB')

# Define los colores a reemplazar y el color de reemplazo
target_colors = [(37, 82, 29), (105, 136, 98), (86, 65, 31)]
lower_replace_range = (226, 226, 226)
replacement_color = (255, 255, 255)

# Convertimos la imagen a un array de numpy para un procesamiento más chido
np_image = np.array(nn_image_rgb)

# Inicializamos la máscara con False
mask = np.zeros(np_image.shape[:2], dtype=bool)

# Iteramos sobre cada color objetivo y actualizamos la máscara
for color in target_colors:
mask |= np.all(np.abs(np_image - color) <= 20, axis=-1)

# Reemplazamos los píxeles encontrados con el color de reemplazo
np_image[mask] = replacement_color

# Encontramos los píxeles en el rango de colores a reemplazar por blanco
replace_mask = np.all((np_image >= lower_replace_range) & (np_image <= replacement_color), axis=-1)

# Reemplazamos los píxeles encontrados con el color blanco
np_image[replace_mask] = replacement_color

# Convertimos el array de nuevo a una imagen PIL
nn_image_result = Image.fromarray(np_image)

# Dividimos la imagen en 10 partes a lo ancho
cantidad_de_cortes = 10
width_per_card = nn_image_result.width // cantidad_de_cortes


# Lista para almacenar las imágenes finales
final_images = []

# Iteramos sobre las partes de la imagen original
for i in range(cantidad_de_cortes):
left = i * width_per_card
right = (i + 1) * width_per_card

# Extraemos cada parte de la imagen original
image_part = nn_image_result.crop((left, 0, right, nn_image_result.height))

# Convertir la parte de la imagen a formato OpenCV (numpy array)
image_array = np.array(image_part)

# Definir el color blanco y la tolerancia permitida
color_blanco = (255, 255, 255)
tolerancia = 30 # Puedes ajustar este valor según tus necesidades

# Calcular el porcentaje de píxeles blancos en la imagen
white_percentage = np.sum(np.all(np.abs(image_array - color_blanco) < tolerancia, axis=-1)) / np.prod(image_array.shape[:-1])

# Si el porcentaje es menor al 72%, añadimos la imagen a la lista final
if white_percentage < 0.83:
final_images.append(image_part)

# Procesamos y guardamos las imágenes que cumplen con la condición
for i, image_part in enumerate(final_images, start=1):
# Procesamos la carta con la red neuronal y almacenamos el resultado
card_result = predict(image_part, self.nn_model, self.table_dict['_class_mapping'])
self.table_cards.append(card_result)
# Guardamos la imagen directamente con el nombre predicho
image_part.save(get_dir('log') + f'/pics/table/{card_result}.png')
log.info(f"Table cards: {self.table_cards}")
if len(self.table_cards) == 1 or len(self.table_cards) == 2:
log.warning(f"Only recognized {len(self.table_cards)} cards on the table. "
f"This can happen if cards are sliding in or if some of the templates are wrong")
return False
return True


def get_dealer_position2(self): # pylint: disable=inconsistent-return-statements
"""Determines position of dealer, where 0=myself, continous counter clockwise"""
for i in range(self.total_players):
Expand Down
58 changes: 47 additions & 11 deletions poker/scraper/table_scraper_nn.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
TRAIN_FOLDER = get_dir('pics', "training_cards")
VALIDATE_FOLDER = get_dir('pics', "validate_cards")
TEST_FOLDER = get_dir('tests', "test_cards")
TEST_FOLDER_TRAIN = get_dir('pics', "test_cards")

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,9 +55,10 @@ def adjust_colors(a, tol=120): # tol - tolerance to decides on the "-ish" facto
class CardNeuralNetwork():

@staticmethod
def create_augmented_images(table_name):
def create_augmented_images(table_name, train_count=600, validate_count=600, test_count=500):
shutil.rmtree(TRAIN_FOLDER, ignore_errors=True)
shutil.rmtree(VALIDATE_FOLDER, ignore_errors=True)
shutil.rmtree(TEST_FOLDER_TRAIN, ignore_errors=True)

log.info("Augmenting data with random pictures based on templates")

Expand All @@ -66,14 +68,14 @@ def create_augmented_images(table_name):
width_shift_range=0.05,
height_shift_range=0.05,
shear_range=0.02,
zoom_range=0.05,
zoom_range=[0.9, 1.5],
horizontal_flip=False,
fill_mode='nearest')

mongo = MongoManager()
table = mongo.get_table(table_name)

for folder in [TRAIN_FOLDER, VALIDATE_FOLDER]:
for folder, count in zip([TRAIN_FOLDER, VALIDATE_FOLDER, TEST_FOLDER_TRAIN], [train_count, validate_count, test_count]):
card_ranks_original = '23456789TJQKA'
original_suits = 'CDHS'

Expand Down Expand Up @@ -101,8 +103,8 @@ def create_augmented_images(table_name):
save_format='png',
):
i += 1
if i > 500:
break # otherwise the generator would loop indefinitely
if i >= count:
break # Limit the number of generated images

def train_neural_network(self):
from tensorflow.keras.preprocessing.image import ImageDataGenerator
Expand All @@ -128,17 +130,31 @@ def train_neural_network(self):
class_mode='binary',
color_mode='rgb')

self.test_generator = ImageDataGenerator(
rescale=0.00,
shear_range=0.00,
zoom_range=0.00,
horizontal_flip=False).flow_from_directory(
directory=os.path.join(SCRAPER_DIR, TEST_FOLDER_TRAIN),
target_size=(img_height, img_width),
batch_size=128,
class_mode='binary',
color_mode='rgb',
shuffle=False)

num_classes = 52
input_shape = (50, 15, 3)
epochs = 50
from tensorflow.keras.callbacks import TensorBoard, EarlyStopping, LearningRateScheduler
from tensorflow.keras.callbacks import TensorBoard, EarlyStopping
from tensorflow.keras.constraints import MaxNorm
from tensorflow.keras.layers import Conv2D, MaxPooling2D, BatchNormalization
from tensorflow.keras.layers import Conv2D, MaxPooling2D
from tensorflow.keras.layers import Dropout, Flatten, Dense
from tensorflow.keras.models import Sequential
from tensorflow.keras.losses import sparse_categorical_crossentropy
from tensorflow.keras import optimizers
from tensorflow.math import exp
from tensorflow.keras.optimizers import Adam

# Configurar el optimizador con una tasa de aprendizaje específica
custom_optimizer = Adam(learning_rate=0.001)

model = Sequential()
model.add(Conv2D(64, (3, 3), input_shape=input_shape, activation='relu', padding='same'))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
Expand All @@ -161,11 +177,13 @@ def train_neural_network(self):
from tensorflow.keras.losses import sparse_categorical_crossentropy
from tensorflow.keras import optimizers
model.compile(loss=sparse_categorical_crossentropy,
optimizer=optimizers.Adam(),
optimizer=custom_optimizer,
metrics=['accuracy'])

log.info(model.summary())

self.model = model

early_stop = EarlyStopping(monitor='val_accuracy',
min_delta=0,
patience=5, # increased patience as sometimes more epochs are beneficial
Expand All @@ -184,9 +202,27 @@ def train_neural_network(self):
validation_data=self.validation_generator,
callbacks=[early_stop])
self.model = model

score = model.evaluate(self.validation_generator, steps=52)
print('Validation loss:', score[0])
print('Validation accuracy:', score[1])
log.info(model.summary())
self.test_neural_network()

def test_neural_network(self):
log.info("Testing the neural network")

if self.model is not None:
# Evaluar el modelo en el conjunto de prueba
test_score = self.model.evaluate(self.test_generator, steps=52)
log.info('Test loss: %f', test_score[0])
log.info('Test accuracy: %f', test_score[1])

# Devolver el porcentaje de resultados de la prueba
return test_score[1]
else:
log.error("Model not trained. Please train the model before testing.")
return None

def save_model_to_disk(self):
# serialize model to JSON
Expand Down
5 changes: 0 additions & 5 deletions poker/scraper/table_screen_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,6 @@ def get_new_hand(self, mouse, h, p):
self.Game_Number = 0
h.game_number_on_screen = 0
self.get_my_funds(h, p)

h.lastGameID = str(h.GameID)
h.GameID = int(round(np.random.uniform(0, 999999999), 0))
cards = ' '.join(self.mycards)
Expand All @@ -581,23 +580,19 @@ def get_new_hand(self, mouse, h, p):
t_algo = threading.Thread(name='Algo', target=self.call_genetic_algorithm, args=(p,))
t_algo.daemon = True
t_algo.start()

self.gui_signals.signal_funds_chart_update.emit(self.game_logger)
self.gui_signals.signal_bar_chart_update.emit(self.game_logger, p.current_strategy)

h.myLastBet = 0
h.myFundsHistory.append(self.myFunds)
h.previousCards = self.mycards
h.lastSecondRoundAdjustment = 0
h.last_round_bluff = False # reset the bluffing marker
h.round_number = 0

mouse.move_mouse_away_from_buttons_jump()
self.take_screenshot(False, p)
else:
log.debug("Game number on screen: " + str(h.game_number_on_screen))
self.get_my_funds(h, p)

return True

def upload_collusion_wrapper(self, p, h):
Expand Down
25 changes: 25 additions & 0 deletions poker/scraper/table_setup_actions_and_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def connect_signals_with_slots(self):
self.ui.topleft_corner.clicked.connect(lambda: self.save_topleft_corner())
self.ui.current_player.currentIndexChanged[int].connect(lambda: self._update_selected_player())
self.ui.use_neural_network.clicked.connect(lambda: self._save_use_nerual_network_checkbox())
self.ui.use_neural_network_table.clicked.connect(lambda: self._save_use_nerual_network_table_checkbox())
self.ui.max_players.currentIndexChanged[int].connect(lambda: self._save_max_players())
self.ui.spinBox_nthSecond.valueChanged.connect(lambda: self._update_nth_second())
self.ui.spinBox_xTimes.valueChanged.connect(lambda: self._update_x_times())
Expand Down Expand Up @@ -231,6 +232,18 @@ def _save_use_nerual_network_checkbox(self):
mongo.update_state(state=is_set, label=label, table_name=self.table_name)
log.info("Saving complete")

def _save_use_nerual_network_table_checkbox(self):
owner = mongo.get_table_owner(self.table_name)
if owner != COMPUTER_NAME:
pop_up("Not authorized.",
"You can only edit your own tables. Please create a new copy or start with a new blank table")
return
label = 'use_neural_network_table'
is_set = self.ui.use_neural_network_table.checkState()
log.info(f"Saving use neural network table tickbox {is_set}")
mongo.update_state(state=is_set, label=label, table_name=self.table_name)
log.info("Saving complete")

def _connect_cards_with_save_slot(self):
# contains cards in the deck
deck = [x.lower() + y.lower() for x in CARD_VALUES for y in CARD_SUITES]
Expand Down Expand Up @@ -739,6 +752,18 @@ def load(self):
log.info(f"No available data for {check_box}")
self.signal_check_box.emit(check_box, 0)

check_boxes = ['use_neural_network_table']
for check_box in check_boxes:
try:
if isinstance(table[check_box], int):
nn = 1 if table[check_box] > 0 else 0
if isinstance(table[check_box], str):
nn = 1 if table[check_box] == 'CheckState.Checked' else 0
self.signal_check_box.emit(check_box, int(nn))
except KeyError:
log.info(f"No available data for {check_box}")
self.signal_check_box.emit(check_box, 0)

try:
all_values = [self.ui.max_players.itemText(i) for i in range(self.ui.max_players.count())]
index = all_values.index(str(table['max_players']['value']))
Expand Down
4 changes: 2 additions & 2 deletions poker/tests/test_montecarlo.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,15 @@ def test_evaluator(
# Unittest to ensure correct winning probabilities are returned
def test_monteCarlo(self): # pylint: disable=too-many-statements
def testRun(Simulation, my_cards, cards_on_table, players, expected_results, opponent_range=1):
maxRuns = 15000 # maximum number of montecarlo runs
maxruns = 15000 # maximum number of montecarlo runs
testRuns = 5 # make several testruns to get standard deviation of winning probability
secs = 1 # cut simulation short if amount of seconds are exceeded

total_result = []
for _ in range(testRuns):
start_time = time.time() + secs
logger = MagicMock()
Simulation.run_montecarlo(logger, my_cards, cards_on_table, players, 1, maxRuns=maxRuns,
Simulation.run_montecarlo(logger, my_cards, cards_on_table, players, 1, max_runs=maxruns,
timeout=start_time, ghost_cards='', opponent_range=opponent_range)
equity = Simulation.equity
total_result.append(equity * 100)
Expand Down
33 changes: 26 additions & 7 deletions poker/tests/test_tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest
from PIL import Image
import os

from poker.scraper.table_scraper_nn import TEST_FOLDER, predict
from poker.tools.helper import ON_CI
Expand All @@ -27,16 +28,34 @@ def test_load_nn_model():

@pytest.mark.skipif(ON_CI, reason='too long for ci')
def test_train_card_neural_network_and_predict():
from poker.scraper.table_scraper_nn import CardNeuralNetwork
from poker.scraper.table_scraper_nn import CardNeuralNetwork, predict
n = CardNeuralNetwork()
n.create_augmented_images('GG Poker2')
n.create_augmented_images('GGpoker')
n.train_neural_network()
n.save_model_to_disk()
n.load_model()

for card in ['AH', '5C', 'QS', '6C', 'JC', '2H']:
filename = f'{TEST_FOLDER}/' + card + '.png'
img = Image.open(filename)
prediction = predict(img, n.loaded_model, n.class_mapping)
log.info(f"Prediction: {prediction} vs actual: {card}")
assert card == prediction
# Obtener el nombre de la carpeta específica
folder_name = card

# Acceder a la carpeta correspondiente
folder_path = os.path.join(TEST_FOLDER, folder_name)

# Buscar archivos en la carpeta
for file_name in os.listdir(folder_path):
if file_name.startswith(card) and file_name.endswith('.png'):
# La imagen coincide con el nombre de la tarjeta
file_path = os.path.join(folder_path, file_name)
img = Image.open(file_path)
prediction = predict(img, n.loaded_model, n.class_mapping)
log.info(f"Prediction: {prediction} vs actual: {card}")
assert card == prediction

elif card in file_name and file_name.endswith('.png'):
# La imagen tiene el nombre similar a "5C_0_27"
file_path = os.path.join(folder_path, file_name)
img = Image.open(file_path)
prediction = predict(img, n.loaded_model, n.class_mapping)
log.info(f"Prediction for similar name: {prediction} vs actual: {card}")
assert card == prediction

0 comments on commit 7228415

Please sign in to comment.