[
  {
    "path": "readme.md",
    "content": "# PokerVision\nRecognition of all entities on the poker table( now only for Pokerstars) and added analytics on the basis of which you can make decisions about your moves \n\n![pokerstars.gif](pokerstars.gif)\n\n\nNOTE: it only works with the theme that I have and for 6 players. It has been tested on ubuntu with python version 3.8\n\n\n# Installing and Running\n\n```\n$ git clone https://github.com/wb-08/PokerVision\n$ cd PokerVision\n$ virtualenv venv\n$ source venv/bin/activate\n$ pip3 install -r requirements.txt\n$ cd scripts\n$ python3 grab_table.py\n```\n"
  },
  {
    "path": "requirements.txt",
    "content": "eval7==0.1.9\nfuture==0.18.3\nmss==9.0.1\nnumpy==1.24.3\nopencv-contrib-python==4.5.5.62\nopencv-python==4.5.5.62\nPillow==9.5.0\npyparsing==3.0.9\nPyYAML==6.0\nCython\n"
  },
  {
    "path": "scripts/__init__.py",
    "content": ""
  },
  {
    "path": "scripts/config.yaml",
    "content": "paths:\n  hero_cards_suits: '../images_templates/hero_cards_suits/'\n  hero_cards_numbers: '../images_templates/hero_cards_numbers/'\n  table_cards_numbers: '../images_templates/table_cards_numbers/'\n  table_cards_suits: '../images_templates/table_cards_suits/'\n  pot_numbers: '../images_templates/pot_numbers/'\n  pot_image: '../images_templates/pot/pot.png'\n  dealer_button: '../images_templates/dealer_button/button.png'\n  empty_seat: '../images_templates/empty_seat/empty_seat.png'\n  sitting_out: '../images_templates/sitting_out/so.png'\n\n\ntable_size:\n  width: 1090\n  height: 900\n\ninfo_box_size:\n  width: 470\n  height: 500\n\nhero_step_define:\n  x_0: 511\n  y_0: 605\n  x_1: 528\n  y_1: 644\n  lower_gray_color: [0, 5, 50]\n  upper_gray_color: [179, 10, 255]\n  min_white_pixels: 400\n\n\nhero_cards:\n  x_0: 478\n  y_0: 535\n  x_1: 616\n  y_1: 592\n  # x coordinate where the card ends\n  separator_1: 67\n  separator_2: 137\n\ntable_cards:\n  x_0: 364\n  y_0: 319\n  x_1: 730\n  y_1: 414\n  # x coordinate where the card ends\n  separator_1: 72\n  separator_2: 144\n  separator_3: 218\n  separator_4: 290\n  separator_5: 363\n\npot:\n  # in what area looking for pot\n  x_0: 472\n  y_0: 279\n  x_1: 636\n  y_1: 328\n  # the maximum width and height over which\n  # the sum of the bet can be placed\n  width: 100\n  height: 20\n  pot_template_width: 33\n\nplayer_center_coordinates:\n  # the first player is the hero and then clockwise\n  # coordinates are stored in [x, y] format\n  1: [469, 598]\n  2: [208,463]\n  3: [237,278]\n  4: [620,200]\n  5: [857, 274]\n  6: [867, 486]\n\nplayers_coordinates:\n  # coordinates are stored in [x_0, y_0, x_1, y_1] format\n  1: [437,592,655,661]\n  2: [21,450, 245,525]\n  3: [49,214, 275,285]\n  4: [437, 139, 659, 213]\n  5: [814, 214, 1040,288]\n  6: [850, 449, 1075, 524]\n\nplayers_bet:\n  # coordinates are stored in [x_0, y_0, x_1, y_1] format\n  2: [279, 457, 379, 480]\n  3: [292, 296, 392, 318]\n  4: [600, 222, 700, 246]\n  5: [688, 270, 788, 292]\n  6: [717, 457, 817, 480]"
  },
  {
    "path": "scripts/equity.py",
    "content": "import numpy as np\nimport eval7\n\n\ndef calc_equity(deck, hero_cards, table_cards, iters=100000):\n    \"\"\"\n    Parameters:\n        deck(list of str): all other cards, not including cards that are on the table and hero cards\n        hero_cards(list of str): cards that belong to the hero\n        table_cards(list of str): cards that are on the table\n        iters(int): the amount that the table generates\n    Returns:\n\n    \"\"\"\n    deck = [eval7.Card(card) for card in deck]\n    table_cards = [eval7.Card(card) for card in table_cards]\n    hero_cards = [eval7.Card(card) for card in hero_cards]\n    max_table_cards = 5\n    win_count = 0\n    for _ in range(iters):\n        np.random.shuffle(deck)\n        num_remaining = max_table_cards - len(table_cards)\n        draw = deck[:num_remaining+2]\n        opp_hole, remaining_comm = draw[:2], draw[2:]\n        player_hand = hero_cards + table_cards + remaining_comm\n        opp_hand = opp_hole + table_cards + remaining_comm\n        player_strength = eval7.evaluate(player_hand)\n        opp_strength = eval7.evaluate(opp_hand)\n\n        if player_strength > opp_strength:\n            win_count += 1\n\n    win_prob = (win_count / iters) * 100\n    return round(win_prob, 2)\n\n"
  },
  {
    "path": "scripts/grab_table.py",
    "content": "from PIL import Image\nimport numpy as np\nimport cv2\nfrom mss import mss\nfrom pokerstars_recognition import PokerStarsTableRecognizer\nfrom utils import read_config_file, set_window_size, remove_cards, data_concatenate\nfrom equity import calc_equity\nfrom info_box import update_label\n\nsct = mss()\nconfig = read_config_file()\nset_window_size()\n\ntable_data = []\nwhile True:\n    updated_table_data = []\n    monitor = {'top': 80, 'left': 70, 'width': config['table_size']['width'],\n               'height': config['table_size']['height']}\n    img = Image.frombytes('RGB', (config['table_size']['width'], config['table_size']['height']),\n                          sct.grab(monitor).rgb)\n    img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)\n    recognizer = PokerStarsTableRecognizer(img, config)\n    hero_step = recognizer.detect_hero_step()\n    if hero_step:\n        hero_cards = recognizer.detect_hero_cards()\n        table_cards = recognizer.detect_table_cards()\n        total_pot = recognizer.find_total_pot()\n        updated_table_data.append([hero_cards, table_cards, total_pot])\n        if table_data == updated_table_data:\n            pass\n        else:\n            table_data = updated_table_data\n            deck = remove_cards(hero_cards, table_cards)\n            equity = calc_equity(deck, hero_cards, table_cards)\n            players_info = recognizer.get_dealer_button_position()\n            players_info = recognizer.get_empty_seats(players_info)\n            players_info = recognizer.get_so_players(players_info)\n            players_info = recognizer.assign_positions(players_info)\n            players_info = recognizer.find_players_bet(players_info)\n            text = data_concatenate(hero_cards, table_cards, total_pot, equity, players_info)\n            update_label(text)\n\n"
  },
  {
    "path": "scripts/info_box.py",
    "content": "from tkinter import *\nfrom scripts.utils import read_config_file\n\ncfg = read_config_file()\nroot = Tk()\nroot.geometry('{0}x{1}'.format(cfg['info_box_size']['width'], cfg['info_box_size']['height']))\nroot.title('PokerStarsHelper')\nroot.configure(background='ivory3')\nlab = Label(root, anchor=\"w\", justify=LEFT, font=(\"Arial\", 18))\nlab.pack(fill=\"both\", expand=True)\n\n\ndef update_label(text):\n    lab.configure(text=text)\n    root.update()"
  },
  {
    "path": "scripts/pokerstars_recognition.py",
    "content": "import cv2\nfrom scripts.table_recognition import PokerTableRecognizer\nfrom scripts.utils import sort_bboxes, thresholding, card_separator, table_part_recognition, \\\n    convert_contours_to_bboxes, find_by_template, find_closer_point, read_config_file\nimport numpy as np\n\n\nclass PokerStarsTableRecognizer(PokerTableRecognizer):\n\n    def __init__(self, img, cfg):\n        \"\"\"\n        Parameters:\n            img(numpy.ndarray): image of the whole table\n            cfg (dict): config file\n        \"\"\"\n        self.img = img\n        self.cfg = cfg\n\n    def detect_hero_step(self):\n        \"\"\"\n        Based on the area under hero's cards,\n        we determine whether hero should make a move\n        Returns:\n            Boolean Value(True or False): True, if hero step now\n        \"\"\"\n        res_img = self.img[self.cfg['hero_step_define']['y_0']:self.cfg['hero_step_define']['y_1'],\n                           self.cfg['hero_step_define']['x_0']:self.cfg['hero_step_define']['x_1']]\n\n        hsv_img = cv2.cvtColor(res_img, cv2.COLOR_BGR2HSV_FULL)\n        mask = cv2.inRange(hsv_img, np.array(self.cfg['hero_step_define']['lower_gray_color']),\n                           np.array(self.cfg['hero_step_define']['upper_gray_color']))\n        count_of_white_pixels = cv2.countNonZero(mask)\n        return True if count_of_white_pixels > self.cfg['hero_step_define']['min_white_pixels'] else False\n\n    def detect_cards(self, separators, sort_bboxes_method, cards_coordinates, path_to_numbers, path_to_suits):\n        \"\"\"\n        Parameters:\n            separators(list of int): contains values where the card ends\n            sort_bboxes_method(str): defines how we will sort the contours.\n            It can be left-to-right, bottom-to-top, top-to-bottom\n            cards_coordinates(str): path to cards coordinates\n            path_to_numbers(str): path where located numbers (J, K etc.)\n            path_to_suits(str) : path where located suits\n        Returns:\n            cards_name(list of str): name of the cards\n        \"\"\"\n        cards_name = []\n        img = self.img[self.cfg[cards_coordinates]['y_0']:self.cfg[cards_coordinates]['y_1'],\n                       self.cfg[cards_coordinates]['x_0']:self.cfg[cards_coordinates]['x_1']]\n        binary_img = thresholding(img, 200, 255)\n        contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        bounding_boxes = convert_contours_to_bboxes(contours, 10, 2)\n        bounding_boxes = sort_bboxes(bounding_boxes, method=sort_bboxes_method)\n        cards_bboxes_dct = card_separator(bounding_boxes, separators)\n        for _, cards_bboxes in cards_bboxes_dct.items():\n            if len(cards_bboxes) == 3:\n                cards_bboxes = [cards_bboxes[0]]\n\n            elif len(cards_bboxes) == 0:\n                return []\n\n            elif len(cards_bboxes) > 3:\n                raise ValueError(\"The number of bounding boxes should not be more than 3!\")\n\n            card_name = ''\n            for key, bbox in enumerate(cards_bboxes):\n                color_of_img, directory = (cv2.IMREAD_COLOR, self.cfg['paths'][path_to_suits]) if key == 0 \\\n                    else (cv2.IMREAD_GRAYSCALE, self.cfg['paths'][path_to_numbers])\n                res_img = img[bbox[1]:bbox[3], bbox[0]:bbox[2]]\n                card_part = table_part_recognition(res_img, directory, color_of_img)\n                card_name = card_part + 'T' if len(cards_bboxes) == 1 else card_name + card_part\n            cards_name.append(card_name[::-1])\n        return cards_name\n\n    def detect_hero_cards(self):\n        \"\"\"\n        Returns:\n            cards_name(list of str): name of the hero's cards\n        \"\"\"\n        separators = [self.cfg['hero_cards']['separator_1'], self.cfg['hero_cards']['separator_2']]\n        sort_bboxes_method = 'bottom-to-top'\n        cards_coordinates = 'hero_cards'\n        path_to_numbers = 'hero_cards_numbers'\n        path_to_suits = 'hero_cards_suits'\n        cards_name = self.detect_cards(separators, sort_bboxes_method, cards_coordinates,\n                                       path_to_numbers, path_to_suits)\n        return cards_name\n\n    def detect_table_cards(self):\n        \"\"\"\n        Returns:\n            cards_name(list of str): name of the cards on the table\n        \"\"\"\n        separators = [self.cfg['table_cards']['separator_1'], self.cfg['table_cards']['separator_2'],\n                      self.cfg['table_cards']['separator_3'], self.cfg['table_cards']['separator_4'],\n                      self.cfg['table_cards']['separator_5']]\n        sort_bboxes_method = 'top-to-bottom'\n        cards_coordinates = 'table_cards'\n        path_to_numbers = 'table_cards_numbers'\n        path_to_suits = 'table_cards_suits'\n        cards_name = self.detect_cards(separators, sort_bboxes_method, cards_coordinates,\n                                       path_to_numbers, path_to_suits)\n        return cards_name\n\n    def find_total_pot(self):\n        \"\"\"\n        Returns:\n            number(str): number with total pot\n        \"\"\"\n        img = self.img[self.cfg['pot']['y_0']:self.cfg['pot']['y_1'],\n              self.cfg['pot']['x_0']:self.cfg['pot']['x_1']]\n        _, max_loc = find_by_template(img, self.cfg['paths']['pot_image'])\n        bet_img = img[max_loc[1] - 3:max_loc[1] + self.cfg['pot']['height'],\n                      max_loc[0] + self.cfg['pot']['pot_template_width']:\n                      max_loc[0] + self.cfg['pot']['pot_template_width'] + self.cfg['pot']['width']]\n        binary_img = thresholding(bet_img, 105, 255)\n        contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        bounding_boxes = convert_contours_to_bboxes(contours, 3, 1)\n        bounding_boxes = sort_bboxes(bounding_boxes, method='left-to-right')\n        number = ''\n        for bbox in bounding_boxes:\n            number_img = bet_img[bbox[1]:bbox[3], bbox[0]:bbox[2]]\n            symbol = table_part_recognition(number_img, self.cfg['paths']['pot_numbers'], cv2.IMREAD_GRAYSCALE)\n            number += symbol\n        return number\n\n    def get_dealer_button_position(self):\n        \"\"\"\n        determine who is closer to the dealer button\n        Returns:\n            player_info(dict): here is information about all players as it becomes available\n        \"\"\"\n        player_info = {key: value for key in range(1, 7) for value in ['']}\n        players_coordinates = self.cfg['player_center_coordinates']\n        _, button_coordinates = find_by_template(self.img, self.cfg['paths']['dealer_button'])\n        player_with_button = find_closer_point(players_coordinates, button_coordinates)\n        player_info[player_with_button] = 'dealer_button'\n        return player_info\n\n    def get_missing_players(self, players_info, path_to_template_img, flag):\n        \"\"\"\n        find players who are currently absent for various reasons\n        Parameters:\n            players_info(dict): key - player number, value - '' - if the player is in the game;\n            '-' - if a player's seat is available; '-so-' - if the player is absent\n            path_to_template_img(str): the path to the images where the benchmark images are located\n            flag(str): It can be - and -so-\n        Returns:\n           players_info(dict): info about players\n        \"\"\"\n        players_coordinates = self.cfg['players_coordinates']\n        players_for_checking = [key for key, value in players_info.items() if value == '']\n        for player, bbox in players_coordinates.items():\n            if player != 1 and player in players_for_checking:\n                player_img = self.img[bbox[1]:bbox[3], bbox[0]:bbox[2]]\n                max_val, _ = find_by_template(player_img, self.cfg['paths'][path_to_template_img])\n                if max_val > 0.8:\n                    players_info[player] = flag\n        return players_info\n\n    def get_empty_seats(self, players_info):\n        \"\"\"\n        find players whose places are currently vacant\n        \"\"\"\n        path_to_template_img = 'empty_seat'\n        flag = '-'\n        players_info = self.get_missing_players(players_info, path_to_template_img, flag)\n        return players_info\n\n    def get_so_players(self, players_info):\n        \"\"\"\n        find players who are not currently in the game\n        \"\"\"\n        path_to_template_img = 'sitting_out'\n        flag = '-so-'\n        players_info = self.get_missing_players(players_info, path_to_template_img, flag)\n        return players_info\n\n    def assign_positions(self, players_info):\n        \"\"\"\n        assign each player one of six positions if the player in the game\n        Parameters:\n            players_info(dict): info about players in {1:'', 2: 'dealer_button',3:'-so-' etc. } format\n        Returns:\n            players_info(dict): info about players in {1: 'BB', 2: 'SB', 3: '-so-' etc. } format\n        \"\"\"\n        busy_seats = [k for k, v in players_info.items() if v != '-' and v != '-so-']\n        exist_positions = ['BTN', 'SB', 'BB', 'UTG', 'MP', 'CO']\n        del exist_positions[3:3 + (6 - len(busy_seats))]\n        player_with_button = [k for k, v in players_info.items() if v == 'dealer_button'][0]\n        for index, player_number in enumerate(\n                busy_seats[busy_seats.index(player_with_button):] + busy_seats[:busy_seats.index(player_with_button)]):\n            if len(busy_seats) == 2:\n                position = 'SB' if index == 0 else 'BB'\n                players_info[player_number] = position\n            else:\n                players_info[player_number] = exist_positions[index]\n        return players_info\n\n    def find_players_bet(self, players_info):\n        \"\"\"\n        Parameters:\n            players_info(dict): info about players in {1:'BTN', 2:'SB', 3:'BB' etc. } format\n        Returns:\n            updated_players_info(dict): info about players in {'Hero':'BTN', 'SB':'', 'BB':'50' etc. } format\n        \"\"\"\n        players_bet_location = self.cfg['players_bet']\n        updated_players_info = {'Hero': players_info[1]}\n        for i, location_coordinates in players_bet_location.items():\n            if players_info[i] not in ('-so-', '-'):\n                bet_img = self.img[location_coordinates[1]:location_coordinates[3],\n                          location_coordinates[0]:location_coordinates[2]]\n                binary_img = thresholding(bet_img, 105, 255)\n                contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n                bounding_boxes = convert_contours_to_bboxes(contours, 3, 1)\n                bounding_boxes = sort_bboxes(bounding_boxes, method='left-to-right')\n                number = ''\n                for bbox in bounding_boxes:\n                    number_img = bet_img[bbox[1]:bbox[3], bbox[0]:bbox[2]]\n                    symbol = table_part_recognition(number_img, self.cfg['paths']['pot_numbers'], cv2.IMREAD_GRAYSCALE)\n                    number += symbol\n                updated_players_info[players_info[i]] = number\n            else:\n                updated_players_info[i] = players_info[i]\n        return updated_players_info\n\n\n"
  },
  {
    "path": "scripts/table_recognition.py",
    "content": "from abc import ABC, abstractmethod\n\n\nclass PokerTableRecognizer(ABC):\n    @abstractmethod\n    def detect_hero_step(self):\n        \"\"\"\n        Based on the area under hero's cards,\n        we determine whether hero should make a move\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def detect_hero_cards(self):\n        pass\n\n    @abstractmethod\n    def detect_table_cards(self):\n        pass\n\n    @abstractmethod\n    def find_total_pot(self):\n        pass\n\n    @abstractmethod\n    def get_dealer_button_position(self):\n        \"\"\"\n        determine who is closer to the dealer button\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_empty_seats(self, players_info):\n        \"\"\"\n        find players whose places are currently vacant\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_so_players(self, players_info):\n        \"\"\"\n        find players who are not currently in the game\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def find_players_bet(self, players_info):\n        pass\n\n\n\n\n\n\n\n"
  },
  {
    "path": "scripts/utils.py",
    "content": "import cv2\nimport numpy as np\nimport yaml\nimport os\nfrom time import sleep\nfrom math import sqrt\n\n\ndef read_config_file(filename='config.yaml'):\n    \"\"\"\n    Parameters:\n        filename (str): config file name\n    Returns: loaded_data (dict)\n    \"\"\"\n    with open(filename, 'r') as stream:\n        loaded_data = yaml.safe_load(stream)\n        return loaded_data\n\n\ndef sort_bboxes(bounding_boxes, method):\n    \"\"\"\n    Parameters:\n         bounding_boxes(list of lists of int): bounding_boxes in [x_0, y_0, x_1, y_1] format\n         method(int): the method of sorting bounding boxes.\n         It can be left-to-right, bottom-to-top or top-to-bottom\n    Returns:\n        bounding_boxes (list of tuple of int): sorted bounding boxes.\n        Each bounding box presented in [x_0, y_0, x_1, y_1] format\n    \"\"\"\n\n    methods = ['left-to-right', 'bottom-to-top', 'top-to-bottom']\n    if method not in methods:\n        raise ValueError(\"Invalid method. Expected one of: %s\" % methods)\n\n    else:\n\n        if method == 'left-to-right':\n            bounding_boxes.sort(key=lambda tup: tup[0])\n\n        elif method == 'bottom-to-top':\n            bounding_boxes.sort(key=lambda tup: tup[1], reverse=True)\n\n        elif method == 'top-to-bottom':\n            bounding_boxes.sort(key=lambda tup: tup[1], reverse=False)\n        return bounding_boxes\n\n\ndef mse(img, benchmark_img):\n    \"\"\"\n    the 'Mean Squared Error' between two images that\n    is the sum of the squared difference between the two images.\n    NOTE: the two images must have the same dimension.\n    Parameters:\n        img(numpy.ndarray): image of a part of the table\n        benchmark_img(numpy.ndarray): benchmark image.\n        This image read from the folder.\n    Returns:\n        err (float): the error between two images\n    \"\"\"\n    err = np.sum((img.astype(\"float\") - benchmark_img.astype(\"float\")) ** 2)\n    err /= float(img.shape[0] * img.shape[1])\n    return err\n\n\ndef image_comparison(img, benchmark_img, color_of_img):\n    \"\"\"\n    Parameters:\n        img(numpy.ndarray): image of a part of the table\n        benchmark_img(str): path to benchmark image\n        color_of_img(int): set in which format to read the image.\n        It can be cv2.IMREAD_COLOR or cv2.IMREAD_GRAYSCALE\n    Returns:\n       err (float): the error between two images\n    \"\"\"\n    colors = [cv2.IMREAD_COLOR, cv2.IMREAD_GRAYSCALE]\n    if color_of_img not in colors:\n        raise ValueError(\"Invalid method. Expected one of: %s\" % colors)\n    benchmark_img = cv2.imread(benchmark_img, color_of_img)\n    res_img = cv2.resize(img, (benchmark_img.shape[1], benchmark_img.shape[0]))\n    if color_of_img == cv2.IMREAD_GRAYSCALE:\n        res_img = cv2.cvtColor(res_img, cv2.COLOR_BGR2GRAY)\n    err = mse(res_img, benchmark_img)\n    return err\n\n\ndef thresholding(img, value_1, value_2):\n    \"\"\"\n    Parameters:\n        img(numpy.ndarray): image of a part of the table\n        value_1(int): threshold value\n        value_2(int): the maximum value that is assigned\n        to pixel values that exceed the threshold value\n    Returns: binary_img(numpy.ndarray)\n    \"\"\"\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    _, binary_img = cv2.threshold(img, value_1, value_2, cv2.THRESH_BINARY)\n    return binary_img\n\n\ndef table_part_recognition(img, directory, color_of_img):\n    \"\"\"\n    Parameters:\n        img(numpy.ndarray): image of a part of the table\n        directory(str): path to the template images\n        color_of_img(int): set in which format to read the image.\n        It can be cv2.IMREAD_COLOR or cv2.IMREAD_GRAYSCALE\n    Returns:\n        table_part(str): recognized suit, value (J, K etc.), pot etc.\n    \"\"\"\n    err_dict = {}\n    for full_image_name in os.listdir(directory):\n        image_name = full_image_name.split('.')[0]\n        err = image_comparison(img, directory + full_image_name, color_of_img)\n        err_dict[image_name] = err\n    table_part = min(err_dict, key=err_dict.get)\n    return table_part\n\n\ndef convert_contours_to_bboxes(contours, min_height, min_width):\n    \"\"\"\n    convert contours to bboxes, also remove all small bounding boxes\n    Parameters:\n        contours (tuple): each individual contour is a numpy array\n         of (x, y) coordinates of boundary points of the object\n        contours(list of tuples of int): bounding boxes suits\n        and values (J, K etc.) in [x, y, w, h] format\n    Returns:\n        cards_bboxes(list of lists of int): bounding boxes suits\n        and values (J, K etc.) in [x_0, y_0, x_1, y_1] format\n    \"\"\"\n    bboxes = [cv2.boundingRect(contour) for contour in contours]\n    cards_bboxes = []\n    for i in range(0, len(bboxes)):\n        x, y, w, h = bboxes[i][0], bboxes[i][1], \\\n            bboxes[i][2], bboxes[i][3]\n        if h >= min_height and w >= min_width:\n            contour_coordinates = [x - 1, y - 1, x + w + 1, y + h + 1]\n            cards_bboxes.append(contour_coordinates)\n    return cards_bboxes\n\n\ndef card_separator(bboxes, separators):\n    \"\"\"\n    determine which bounding box belongs to which card\n    Parameters:\n        bboxes(list of lists of int): bounding boxes suits and values (J, K etc.)\n        separators(list of int): contains values where the card ends\n    Returns:\n       sorted_dct(dict) key - card number, value - bounding boxes\n    \"\"\"\n    dct = {}\n    for bbox in bboxes:\n        for separator in separators:\n            if bbox[2] < separator:\n                dct[separators.index(separator)] = dct.get(separators.index(separator), []) + [bbox]\n                break\n    sorted_dct = {key: value for key, value in sorted(dct.items(), key=lambda item: int(item[0]))}\n    return sorted_dct\n\n\ndef find_by_template(img, path_to_image):\n    \"\"\"\n     Object detection using a \"template\".\n     Parameters:\n         img(numpy.ndarray): image of a part of the table/of the entire table\n         path_to_image(str): the path to a template image\n    Returns:\n        max_val(float): largest value with the most likely match\n        max_loc(tuple of int): location with the largest value.\n        Location is presented in (x, y) format\n    \"\"\"\n    template_img_gray = cv2.imread(path_to_image, 0)\n    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    result = cv2.matchTemplate(img_gray, template_img_gray,\n                               cv2.TM_CCOEFF_NORMED)\n    (min_val, max_val, min_loc, max_loc) = cv2.minMaxLoc(result)\n    return max_val, max_loc\n\n\ndef load_images(directory):\n    \"\"\"\n    Parameters:\n        directory(str): path to specific directory\n    Returns:\n        images(list of numpy.ndarray): images of a part of the table\n        file_names(list of str): the name of the files in the folder\n    \"\"\"\n    images = [cv2.imread(directory + file) for file in sorted(os.listdir(directory))]\n    file_names = [file for file in sorted(os.listdir(directory))]\n    return images, file_names\n\n\ndef find_closer_point(players_coordinates, button_coordinates):\n    \"\"\"\n    find the distance between two points  and find the minimum distance\n    Parameters:\n        players_coordinates(dict): key - player number, value - player coordinates\n        button_coordinates(tuple): Coordinates are presented in (x, y) format\n    Returns:\n        player_with_button(int): the number of the player who is closest to the buttonthe number of the player who is closest to the button\n    \"\"\"\n    distance_dict = {}\n    for player, player_coordinates in players_coordinates.items():\n        distance = sqrt((player_coordinates[0] - button_coordinates[0]) ** 2\n                        + (player_coordinates[1] - button_coordinates[1]) ** 2)\n        distance_dict[player] = distance\n    player_with_button = min(distance_dict, key=distance_dict.get)\n    return player_with_button\n\n\ndef remove_cards(hero_cards, table_cards):\n    \"\"\"\n    Parameters:\n        hero_cards(list of str): cards that belong to the hero\n        table_cards(list of str): cards that are on the table\n    Returns:\n        all_cards(list of str): all other cards, not including cards that are on the table and hero cards\n    \"\"\"\n    all_cards = ['2c', '2d', '2h', '2s', '3c', '3d', '3h', '3s', '4c', '4d', '4h', '4s', '5c', '5d', '5h', '5s',\n                 '6c', '6d', '6h', '6s', '7c', '7d', '7h', '7s', '8c', '8d', '8h', '8s', '9c', '9d', '9h', '9s',\n                 'Tc', 'Td', 'Th', 'Ts', 'Jc', 'Jd', 'Jh', 'Js', 'Qc', 'Qd', 'Qh', 'Qs', 'Kc', 'Kd', 'Kh', 'Ks',\n                 'Ac', 'Ad', 'Ah', 'As']\n    for card in hero_cards+table_cards:\n        all_cards.remove(card)\n    return all_cards\n\n\ndef set_window_size():\n    \"\"\"\n    set the application window in the right place and with the right size\n    \"\"\"\n    sleep(3)\n    cmd = 'wmctrl -r :ACTIVE: -e 0,0,0,1100,900'\n    os.system(cmd)\n\n\ndef data_concatenate(hero_hand, table_cards, total_pot, equity, players_info):\n    \"\"\"\n    information from all lists, dictionaries are added to one common line\n    \"\"\"\n    text_players_info = ''\n    for key, value in players_info.items():\n        value = 'no bet found' if value == '' else value\n        text_players_info += str(key) + ':' + str(value) + '\\n'\n\n    table_cards = ['no cards on the table'] if table_cards == [] else table_cards\n\n    text = 'Hero hand: ' + ' '.join(hero_hand) + '\\n' + 'Board: ' + ' '.join(table_cards) + '\\n' + \\\n           'Pot: ' + total_pot + '\\n' + 'Equity: ' + str(equity) + '%' + '\\n' + \\\n           '------------------------------' + '\\n' + text_players_info\n    return text\n\n\n"
  },
  {
    "path": "unittests/TestTableRecognition.py",
    "content": "import unittest\nfrom scripts.pokerstars_recognition import PokerStarsTableRecognizer\nfrom scripts.utils import read_config_file, load_images\n\ncfg = read_config_file('../scripts/config.yaml')\ntest_cfg = read_config_file('test_config.yaml')\n\n\nclass TestTableRecognition(unittest.TestCase):\n\n    def test_hero_step(self):\n        images, file_names = load_images(test_cfg['paths']['hero_step'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestHeroStep Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                self.assertEqual(recognizer.detect_hero_step(), test_cfg['hero_step'][filename])\n\n    def test_hero_cards(self):\n        images, file_names = load_images(test_cfg['paths']['hero_cards'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestHeroCards Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                self.assertEqual(recognizer.detect_hero_cards(), test_cfg['hero_cards'][filename])\n\n    def test_table_cards(self):\n        images, file_names = load_images(test_cfg['paths']['table_cards'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestTableCards Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                self.assertEqual(recognizer.detect_table_cards(), test_cfg['table_cards'][filename])\n\n    def test_total_pot(self):\n        images, file_names = load_images(test_cfg['paths']['total_pot'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestTotalPot Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                self.assertEqual(recognizer.find_total_pot(), test_cfg['total_pot'][filename])\n\n    def test_dealer_button_position(self):\n        images, file_names = load_images(test_cfg['paths']['dealer_button_position'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestDealerButton Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                self.assertEqual(recognizer.get_dealer_button_position(), test_cfg['dealer_button_position'][filename])\n\n    def test_player_position(self):\n        images, file_names = load_images(test_cfg['paths']['player_position'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestPlayerPosition Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                players_info = recognizer.get_dealer_button_position()\n                players_info = recognizer.get_empty_seats(players_info)\n                players_info = recognizer.get_so_players(players_info)\n                self.assertEqual(recognizer.assign_positions(players_info), test_cfg['player_position'][filename])\n\n    def test_player_bet(self):\n        images, file_names = load_images(test_cfg['paths']['player_bet'])\n        for image, filename in zip(images, file_names):\n            with self.subTest(\"TestPlayerBet Incorrect detection in the image\", filename=filename):\n                recognizer = PokerStarsTableRecognizer(image, cfg)\n                players_info = recognizer.get_dealer_button_position()\n                players_info = recognizer.get_empty_seats(players_info)\n                players_info = recognizer.get_so_players(players_info)\n                players_info = recognizer.assign_positions(players_info)\n                self.assertEqual(recognizer.find_players_bet(players_info), test_cfg['player_bet'][filename])\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "unittests/test_config.yaml",
    "content": "paths:\n  hero_step: 'images_for_test/hero_step/'\n  hero_cards: 'images_for_test/hero_cards/'\n  table_cards: 'images_for_test/table_cards/'\n  total_pot: 'images_for_test/total_pot/'\n  dealer_button_position: 'images_for_test/dealer_button_position/'\n  player_position: 'images_for_test/player_position/'\n  player_bet: 'images_for_test/player_bet/'\n\nhero_step:\n  test_image_1.png: True\n  test_image_2.png: False\n  test_image_3.png: True\n  test_image_4.png: False\n  test_image_5.png: False\n  test_image_6.png: False\n  test_image_7.png: False\n  test_image_8.png: True\n  test_image_9.png: True\n  test_image_10.png: False\n  test_image_11.png: False\n  test_image_12.png: True\n  test_image_13.png: True\n  test_image_14.png: False\n  test_image_15.png: False\n  test_image_16.png: False\n  test_image_17.png: True\n  test_image_18.png: False\n  test_image_19.png: True\n\nhero_cards:\n  test_image_1.png: ['Qs', 'As']\n  test_image_2.png: []\n  test_image_3.png: ['7c','Kh']\n  test_image_4.png: []\n  test_image_5.png: ['2h','Ks']\n  test_image_6.png: ['2h','Ks']\n  test_image_7.png: ['Ah', '4h']\n  test_image_8.png: ['Ah', '4h']\n  test_image_9.png: ['2d', '6s']\n  test_image_10.png: ['Qd', '3s']\n  test_image_11.png: ['Kc', 'Ad']\n  test_image_12.png: ['As', 'Td']\n  test_image_13.png: ['5s', '9c']\n  test_image_14.png: ['Kd', 'Kh']\n  test_image_15.png: ['Ad', 'Td']\n  test_image_16.png: ['2d', 'Tc']\n\ntable_cards:\n  test_image_1.png: ['9h','5d','6d','9c']\n  test_image_2.png: ['6c','4s','Ts','2s','8s']\n  test_image_3.png: ['7h','4c','5h','Ac','Td']\n  test_image_4.png: ['2d','3s','7h','Qh','Ah']\n  test_image_5.png: ['Qc','Kd','2s']\n  test_image_6.png: ['Th','Jc','Qc']\n  test_image_7.png: ['Th','Jc','Qc','Jh']\n  test_image_8.png: ['2s','Qh','Qs']\n  test_image_9.png: ['2s','Qh','Qs', 'Ah']\n  test_image_11.png: ['2s','Qh','Qs', 'Ah','Ac']\n  test_image_12.png: ['Tc','9h','8d']\n  test_image_13.png: ['5c','Qs','3d','2s']\n  test_image_14.png: ['5c','Qs','3d','2s', 'Ah']\n  test_image_15.png: ['Ad','Tc','4s','7h','3h']\n  test_image_16.png: ['4c','7d','9d']\n  test_image_17.png: ['9s','6c','7s','Jh']\n  test_image_18.png: ['6d','Tc','5s']\n  test_image_19.png: []\n  test_image_20.png: ['3h','Qc','7c']\n\ntotal_pot:\n  test_image_1.png: '1,550'\n  test_image_2.png: '150'\n  test_image_3.png: '2,268'\n  test_image_4.png: '250'\n  test_image_5.png: '861'\n  test_image_7.png: '3,496'\n  test_image_8.png: '17,450'\n  test_image_9.png: '350'\n  test_image_10.png: '2,354'\n  test_image_11.png: '400'\n  test_image_12.png: '750'\n  test_image_13.png: '550'\n  test_image_14.png: '38,169'\n  test_image_15.png: '283'\n  test_image_16.png: '478'\n\ndealer_button_position:\n  test_image_1.png: {1: '', 2: 'dealer_button', 3: '', 4: '', 5: '', 6: ''}\n  test_image_2.png: {1: '', 2: '', 3: 'dealer_button', 4: '', 5: '', 6: ''}\n  test_image_3.png: {1: '', 2: '', 3: '', 4: 'dealer_button', 5: '', 6: ''}\n  test_image_4.png: {1: '', 2: '', 3: '', 4: '', 5: '', 6: 'dealer_button'}\n  test_image_5.png: {1: '', 2: '', 3: '', 4: 'dealer_button', 5: '', 6: ''}\n  test_image_6.png: {1: 'dealer_button', 2: '', 3: '', 4: '', 5: '', 6: ''}\n  test_image_7.png: {1: 'dealer_button', 2: '', 3: '', 4: '', 5: '', 6: ''}\n  test_image_8.png: {1: '', 2: '', 3: '', 4: '', 5: 'dealer_button', 6: ''}\n  test_image_9.png: {1: '', 2: '', 3: 'dealer_button', 4: '', 5: '', 6: ''}\n  test_image_10.png: {1: '', 2: '', 3: '', 4: '', 5: '', 6: 'dealer_button'}\n  test_image_11.png: {1: '', 2: '', 3: '', 4: 'dealer_button', 5: '', 6: ''}\n\nplayer_position:\n  test_image_1.png: {1: 'CO', 2: 'BTN', 3: 'SB', 4: 'BB', 5: '-so-', 6: 'MP'}\n  test_image_2.png: {1: 'MP', 2: 'CO', 3: 'BTN', 4: 'SB', 5: '-so-', 6: 'BB'}\n  test_image_3.png: {1: 'BB', 2: '-so-', 3: 'CO', 4: 'BTN', 5: '-so-', 6: 'SB'}\n  test_image_4.png: {1: 'SB', 2: 'BB', 3: 'MP', 4: 'CO', 5: '-so-', 6: 'BTN'}\n  test_image_5.png: {1: 'BB', 2: 'CO', 3: '-so-', 4: '-so-', 5: 'BTN', 6: 'SB'}\n  test_image_6.png: {1: 'BB', 2: 'MP', 3: 'CO', 4: 'BTN', 5: '-so-', 6: 'SB'}\n  test_image_7.png: {1: 'BB', 2: '-so-', 3: '-so-', 4: '-', 5: '-', 6: 'SB'}\n  test_image_8.png: {1: 'BB', 2: '-', 3: 'CO', 4: 'BTN', 5: '-', 6: 'SB'}\n  test_image_9.png: {1: 'BTN', 2: 'SB', 3: 'BB', 4: 'UTG', 5: 'MP', 6: 'CO'}\n  test_image_10.png: {1: 'BB', 2: '-', 3: 'CO', 4: 'BTN', 5: '-', 6: 'SB'}\n\nplayer_bet:\n  test_image_1.png: {'Hero': 'CO', 'BTN': '', 'SB': '700', 'BB': '', 5: '-so-', 'MP': ''}\n  test_image_2.png: {'Hero': 'SB', BB: '8,000', MP: '', CO: '', 5: '-so-', BTN: ''}\n  test_image_3.png: {'Hero': 'BB', 2: '-so-', CO: '', BTN: '', 5: '-so-', SB: '50'}\n  test_image_4.png: {'Hero': 'CO', BTN: '1,748', SB: '1,748', BB: '', 5: '-so-', MP: ''}\n  test_image_5.png: {'Hero': 'SB', 2: '-', 'BB': '100', 'CO': '', 5: '-', BTN: '100'}\n  test_image_6.png: {'Hero': 'BTN', 'SB': '50', 'BB': '100', 'MP': '100', 5: '-so-', CO: '100'}\n  test_image_7.png: {'Hero': 'SB', 'BB': '', 3: '-so-', 4: '-so-', 'CO': '', 'BTN': ''}\n  test_image_8.png: {'Hero': 'BB', 2: '-', 'CO': '', 'BTN': '100', 5: '-', 'SB': ''}\n  test_image_9.png: {'Hero': 'BTN', 'SB': '', 'BB': '50', 'UTG': '100', 'MP': '200', 'CO': '200'}\n  test_image_10.png: {'Hero': 'BB', 'CO': '2,362', 3: '-', 4: '-', 'BTN': '', 'SB': ''}\n\n\n\n\n\n"
  }
]