From 2a05095c49f50078e2d8aa6a85aeb195ccc0daf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20W=C3=B6lfer?= Date: Sun, 9 Feb 2020 19:37:09 +0100 Subject: [PATCH] Added virtenv --- Pipfile | 12 ++ .../card_detection/board_parser.py | 149 ++++++++++++++++-- test/test_cv.py | 89 ++++++++++- .../border.py} | 0 .../bunker.py} | 0 .../{generate_config.py => generate/field.py} | 0 tools/{generate_goal.py => generate/goal.py} | 0 tools/{generate_hua.py => generate/hua.py} | 0 .../special_buttons.py} | 0 9 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 Pipfile rename tools/{generate_border.py => generate/border.py} (100%) rename tools/{generate_bunker.py => generate/bunker.py} (100%) rename tools/{generate_config.py => generate/field.py} (100%) rename tools/{generate_goal.py => generate/goal.py} (100%) rename tools/{generate_hua.py => generate/hua.py} (100%) rename tools/{generate_special_buttons.py => generate/special_buttons.py} (100%) diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..85b73bd --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +pyautogui = "*" + +[requires] +python_version = "3.8" diff --git a/shenzhen_solitaire/card_detection/board_parser.py b/shenzhen_solitaire/card_detection/board_parser.py index 729f55c..f20274e 100644 --- a/shenzhen_solitaire/card_detection/board_parser.py +++ b/shenzhen_solitaire/card_detection/board_parser.py @@ -1,14 +1,15 @@ """Contains parse_board function""" +import copy import itertools -from typing import Any, Iterable, List, Optional, Tuple, Union, Dict +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union import cv2 import numpy as np from ..board import Board, Card, NumberCard, SpecialCard -from . import card_finder -from .configuration import Configuration +from . import adjustment, card_finder +from .configuration import Configuration, ButtonState def grouper( @@ -19,20 +20,27 @@ def grouper( return itertools.zip_longest(*args, fillvalue=fillvalue) +def fake_adjustment(adj: adjustment.Adjustment) -> adjustment.Adjustment: + result = copy.deepcopy(adj) + result.x -= 5 + result.y -= 5 + result.h += 10 + result.w += 10 + return result + + def get_field_square_iterator( image: np.ndarray, conf: Configuration, row_count: int, column_count: int ) -> Iterable[Tuple[np.ndarray, np.ndarray]]: """Return iterator for both the square, as well as the matching card border""" - fake_adjustments = conf.field_adjustment - fake_adjustments.x -= 5 - fake_adjustments.y -= 5 - fake_adjustments.h += 10 - fake_adjustments.w += 10 + my_adj = fake_adjustment(conf.field_adjustment) + my_border_adj = fake_adjustment(conf.border_adjustment) + squares = card_finder.get_field_squares( - image, fake_adjustments, count_x=row_count, count_y=column_count + image, my_adj, count_x=row_count, count_y=column_count ) border_squares = card_finder.get_field_squares( - image, conf.border_adjustment, count_x=row_count, count_y=column_count + image, my_border_adj, count_x=row_count, count_y=column_count ) grouped_squares = grouper(squares, row_count) grouped_border_squares = grouper(border_squares, row_count) @@ -96,23 +104,132 @@ def parse_field(image: np.ndarray, conf: Configuration) -> List[List[Card]]: def parse_hua(image: np.ndarray, conf: Configuration) -> bool: """Return true if hua is in the hua spot, false if hua spot is empty""" - raise NotImplementedError() + my_hua_adj = fake_adjustment(conf.hua_adjustment) + hua_square = card_finder.get_field_squares(image, my_hua_adj, count_x=1, count_y=1)[ + 0 + ] + hua_templates = [ + image for image, card_type in conf.catalogue if card_type == SpecialCard.Hua + ] + best_hua = max( + match_template(template=template, search_image=hua_square) + for template in hua_templates + ) + best_green = max( + match_template(template=template, search_image=hua_square) + for template in conf.green_card + ) + return best_hua > best_green + + +def parse_bunker_field( + image: np.ndarray, + green_cards: List[np.ndarray], + card_backs: List[np.ndarray], + catalogue: List[Tuple[np.ndarray, Card]], +) -> Union[Tuple[SpecialCard, int], Optional[Card]]: + + best_green = max( + match_template(template=template, search_image=image) + for template in green_cards + ) + best_back = max( + match_template(template=template, search_image=image) for template in card_backs + ) + + best_card_value, best_card_name = max( + ((match_template(template, image), name) for template, name in catalogue), + key=lambda x: x[0], + ) + + return max( + [ + (best_green, None), + (best_back, (SpecialCard.Hua, 0)), + (best_card_value, best_card_name), + ], + key=lambda x: x[0], + )[1] + + +def parse_special_button( + image: np.ndarray, + position: SpecialCard, + buttons: List[Tuple[ButtonState, SpecialCard, np.ndarray]], +) -> ButtonState: + """Return true if special button is greyed out, e.g. this dragon card is removed from the field""" + square_fits = [ + (match_template(template, image), state, name) + for state, name, template in buttons + ] + best_state, best_name = max(square_fits, key=lambda x: x[0])[1:] + assert best_name == position + return best_state def parse_bunker( image: np.ndarray, conf: Configuration ) -> List[Union[Tuple[SpecialCard, int], Optional[Card]]]: - raise NotImplementedError() + bunker_squares = card_finder.get_field_squares( + image, fake_adjustment(conf.bunker_adjustment), count_x=1, count_y=3 + ) + button_squares = card_finder.get_field_squares( + image, fake_adjustment(conf.special_button_adjustment), count_x=3, count_y=1 + ) + dragon_sequence = [SpecialCard.Zhong, SpecialCard.Fa, SpecialCard.Bai] + dragons = [ + card_type + for dragon_image, card_type in zip(button_squares, dragon_sequence) + if parse_special_button(dragon_image, card_type, conf.special_buttons) + == ButtonState.greyed + ] + dragon_iter = iter(dragons) + matches = [ + parse_bunker_field(square, conf.green_card, conf.card_back, conf.catalogue) + for square in bunker_squares + ] + matches = [(next(dragon_iter), 0) if isinstance(x, tuple) else x for x in matches] + assert next(dragon_iter, None) is None + return matches + + +def parse_goal_field( + image: np.ndarray, + catalogue: List[Tuple[np.ndarray, Card]], + green_cards: List[np.ndarray], +) -> Optional[NumberCard]: + square_fits = [ + (match_template(template, image), name) for template, name in catalogue + ] + best_card_value, best_card_name = max(square_fits, key=lambda x: x[0]) + + best_green_value = max(match_template(template, image) for template in green_cards) + if best_green_value > best_card_value: + return None + + assert isinstance(best_card_name, NumberCard) + return best_card_name def parse_goal(image: np.ndarray, conf: Configuration) -> Dict[NumberCard.Suit, int]: - raise NotImplementedError() + goal_squares = card_finder.get_field_squares( + image, fake_adjustment(conf.goal_adjustment), count_x=1, count_y=3 + ) + goal_list = [ + parse_goal_field(square, conf.catalogue, conf.green_card) + for square in goal_squares + ] + base_goal_dict = {suit: 0 for suit in NumberCard.Suit} + base_goal_dict.update( + {x.suit: x.number for x in (x for x in goal_list if x is not None)} + ) + return base_goal_dict def parse_board(image: np.ndarray, conf: Configuration) -> Board: result = Board() result.field = parse_field(image, conf) - # result.flower_gone = parse_hua(image, conf) - # result.bunker = parse_bunker(image, conf) - # result.goal = parse_goal(image, conf) + result.flower_gone = parse_hua(image, conf) + result.bunker = parse_bunker(image, conf) + result.goal = parse_goal(image, conf) return result diff --git a/test/test_cv.py b/test/test_cv.py index b85307f..870dfab 100644 --- a/test/test_cv.py +++ b/test/test_cv.py @@ -1,13 +1,17 @@ """Contains function to manually test the visual detection of a board""" +import copy import unittest +from typing import List, Tuple, Union import cv2 import numpy as np -from shenzhen_solitaire import board -from shenzhen_solitaire.card_detection import adjustment, board_parser import shenzhen_solitaire.card_detection.configuration as configuration +from shenzhen_solitaire import board +from shenzhen_solitaire.board import Card, NumberCard, SpecialCard +from shenzhen_solitaire.card_detection import adjustment, board_parser + from . import boards @@ -21,3 +25,84 @@ class CardDetectionTest(unittest.TestCase): for correct_row, my_row in zip(boards.B20190809172206_1.field, my_board.field): self.assertListEqual(correct_row, my_row) + + def test_hua_detection(self) -> None: + """Read a board and check if it can detect if the flower is gone""" + loaded_config = configuration.load("test_config.zip") + imagenames = [ + ("BaiBlack", False), + ("BaiShiny", True), + ("BunkerCards", True), + ("FaShiny", False), + ("ZhongShiny", False), + ] + for imagename, flower_gone in imagenames: + image = cv2.imread(f"pictures/specific/{imagename}.jpg") + my_board = board_parser.parse_board(image, loaded_config) + self.assertEqual(flower_gone, my_board.flower_gone) + + def test_bunker_parsing(self) -> None: + loaded_config = configuration.load("test_config.zip") + imagenames: List[ + Tuple[str, List[Union[Tuple[SpecialCard, int], Card, None]]] + ] = [ + ( + "BaiBlack", + [(SpecialCard.Bai, 0), None, NumberCard(NumberCard.Suit.Green, 3)], + ), + ( + "BaiShiny", + [(SpecialCard.Zhong, 0), SpecialCard.Bai, (SpecialCard.Fa, 0)], + ), + ( + "BunkerCards", + [ + NumberCard(NumberCard.Suit.Black, 6), + NumberCard(NumberCard.Suit.Green, 9), + NumberCard(NumberCard.Suit.Green, 8), + ], + ), + ("FaShiny", [None, NumberCard(NumberCard.Suit.Green, 6), SpecialCard.Fa]), + ( + "ZhongShiny", + [ + (SpecialCard.Fa, 0), + NumberCard(NumberCard.Suit.Green, 6), + SpecialCard.Zhong, + ], + ), + ] + for imagename, bunker in imagenames: + image = cv2.imread(f"pictures/specific/{imagename}.jpg") + my_board = board_parser.parse_board(image, loaded_config) + self.assertListEqual(bunker, my_board.bunker) + + def test_goal_parsing(self) -> None: + loaded_config = configuration.load("test_config.zip") + imagenames: List[Tuple[str, List[NumberCard]]] = [ + ("BaiBlack", [NumberCard(NumberCard.Suit.Green, 2)],), + ( + "BaiShiny", + [ + NumberCard(NumberCard.Suit.Green, 3), + NumberCard(NumberCard.Suit.Red, 2), + NumberCard(NumberCard.Suit.Black, 3), + ], + ), + ( + "BunkerCards", + [ + NumberCard(NumberCard.Suit.Red, 1), + NumberCard(NumberCard.Suit.Black, 1), + ], + ), + ("FaShiny", [NumberCard(NumberCard.Suit.Green, 2)]), + ("ZhongShiny", [NumberCard(NumberCard.Suit.Green, 2)]), + ] + base_goal_dict = {suit: 0 for suit in NumberCard.Suit} + for imagename, goal in imagenames: + image = cv2.imread(f"pictures/specific/{imagename}.jpg") + my_goal_dict = copy.deepcopy(base_goal_dict) + my_goal_dict.update({x.suit: x.number for x in goal}) + my_board = board_parser.parse_board(image, loaded_config) + self.assertDictEqual(my_goal_dict, my_board.goal) diff --git a/tools/generate_border.py b/tools/generate/border.py similarity index 100% rename from tools/generate_border.py rename to tools/generate/border.py diff --git a/tools/generate_bunker.py b/tools/generate/bunker.py similarity index 100% rename from tools/generate_bunker.py rename to tools/generate/bunker.py diff --git a/tools/generate_config.py b/tools/generate/field.py similarity index 100% rename from tools/generate_config.py rename to tools/generate/field.py diff --git a/tools/generate_goal.py b/tools/generate/goal.py similarity index 100% rename from tools/generate_goal.py rename to tools/generate/goal.py diff --git a/tools/generate_hua.py b/tools/generate/hua.py similarity index 100% rename from tools/generate_hua.py rename to tools/generate/hua.py diff --git a/tools/generate_special_buttons.py b/tools/generate/special_buttons.py similarity index 100% rename from tools/generate_special_buttons.py rename to tools/generate/special_buttons.py