From 07f8cae23847d02ea551631baaf3bf0dd2aab5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20W=C3=B6lfer?= Date: Sat, 8 Feb 2020 00:42:55 +0100 Subject: [PATCH] Made border detection work --- shenzhen_solitaire/board.py | 3 +- .../card_detection/adjustment.py | 53 ++++++++------- .../card_detection/board_parser.py | 25 ++++--- .../card_detection/configuration.py | 62 +++++++++++++----- test/test_cv.py | 10 ++- test_config.zip | Bin 45657 -> 49012 bytes tools/generate_border.py | 20 ++++-- 7 files changed, 112 insertions(+), 61 deletions(-) diff --git a/shenzhen_solitaire/board.py b/shenzhen_solitaire/board.py index d9f403d..2283498 100644 --- a/shenzhen_solitaire/board.py +++ b/shenzhen_solitaire/board.py @@ -53,9 +53,10 @@ class Board: # Starting max row is 5, if the last one is a `1`, we can put a `2` - `9` on top of it, resulting in 13 cards MAX_ROW_SIZE = 13 + MAX_COLUMN_SIZE = 8 def __init__(self) -> None: - self.field: List[List[Card]] = [[]] * 8 + self.field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE self.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3 self.goal: Dict[NumberCard.Suit, int] = { NumberCard.Suit.Red: 0, diff --git a/shenzhen_solitaire/card_detection/adjustment.py b/shenzhen_solitaire/card_detection/adjustment.py index 8a5c083..0fd3596 100644 --- a/shenzhen_solitaire/card_detection/adjustment.py +++ b/shenzhen_solitaire/card_detection/adjustment.py @@ -10,31 +10,36 @@ import cv2 @dataclass class Adjustment: """Configuration for a grid""" - x: int - y: int - w: int - h: int - dx: int - dy: int + + x: int = 0 + y: int = 0 + w: int = 0 + h: int = 0 + dx: int = 0 + dy: int = 0 -def get_square(adjustment: Adjustment, index_x: int = 0, - index_y: int = 0) -> Tuple[int, int, int, int]: +def get_square( + adjustment: Adjustment, index_x: int = 0, index_y: int = 0 +) -> Tuple[int, int, int, int]: """Get one square from index and adjustment""" - return (adjustment.x + adjustment.dx * index_x, - adjustment.y + adjustment.dy * index_y, - adjustment.x + adjustment.w + adjustment.dx * index_x, - adjustment.y + adjustment.h + adjustment.dy * index_y) + return ( + adjustment.x + adjustment.dx * index_x, + adjustment.y + adjustment.dy * index_y, + adjustment.x + adjustment.w + adjustment.dx * index_x, + adjustment.y + adjustment.h + adjustment.dy * index_y, + ) def adjust_squares( - image: numpy.ndarray, - count_x: int, - count_y: int, - adjustment: Optional[Adjustment] = None) -> Adjustment: + image: numpy.ndarray, + count_x: int, + count_y: int, + adjustment: Optional[Adjustment] = None, +) -> Adjustment: if not adjustment: - adjustment = Adjustment(0, 0, 0, 0, 0, 0) + adjustment = Adjustment(w=10, h=10) def _adjustment_step(keycode: int) -> None: assert adjustment is not None @@ -59,21 +64,19 @@ def adjust_squares( while True: working_image = image.copy() - for index_x, index_y in itertools.product( - range(count_x), range(count_y)): + for index_x, index_y in itertools.product(range(count_x), range(count_y)): square = get_square(adjustment, index_x, index_y) - cv2.rectangle(working_image, - (square[0], square[1]), - (square[2], square[3]), - (0, 0, 0)) - cv2.imshow('Window', working_image) + cv2.rectangle( + working_image, (square[0], square[1]), (square[2], square[3]), (0, 0, 0) + ) + cv2.imshow("Window", working_image) keycode = cv2.waitKey(0) print(keycode) if keycode == 27: break _adjustment_step(keycode) - cv2.destroyWindow('Window') + cv2.destroyWindow("Window") return adjustment diff --git a/shenzhen_solitaire/card_detection/board_parser.py b/shenzhen_solitaire/card_detection/board_parser.py index 401605e..e2774d5 100644 --- a/shenzhen_solitaire/card_detection/board_parser.py +++ b/shenzhen_solitaire/card_detection/board_parser.py @@ -2,7 +2,7 @@ import numpy as np from .configuration import Configuration -from ..board import Board, NumberCard, SpecialCard +from ..board import Board, NumberCard, SpecialCard, Card from . import card_finder import cv2 from typing import Iterable, Any, List, Tuple, Union @@ -37,12 +37,13 @@ def get_square_iterator( return zip(grouped_squares, grouped_border_squares) -def match_template(template: np.ndarray, search_image: np.ndarray) -> int: +def match_template(template: np.ndarray, search_image: np.ndarray) -> float: """Return matchiness for the template on the search image""" + res = cv2.matchTemplate(search_image, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) - assert isinstance(max_val, int) - return max_val + assert isinstance(max_val, (int, float)) + return float(max_val) def parse_square( @@ -70,13 +71,13 @@ def parse_square( return (best_name, row_finished) -def parse_board(image: np.ndarray, conf: Configuration) -> Board: +def parse_field(image: np.ndarray, conf: Configuration) -> List[List[Card]]: """Parse a screenshot of the game, using a given configuration""" square_iterator = get_square_iterator( - image, conf, row_count=Board.MAX_ROW_SIZE, column_count=8 + image, conf, row_count=Board.MAX_ROW_SIZE, column_count=Board.MAX_COLUMN_SIZE ) - result = Board() - for group_index, (square_group, border_group) in enumerate(square_iterator): + result = [] + for square_group, border_group in square_iterator: group_field = [] for index, (square, border_square) in enumerate( zip(square_group, border_group) @@ -86,6 +87,12 @@ def parse_board(image: np.ndarray, conf: Configuration) -> Board: if row_finished: break - result.field[group_index] = group_field + result.append(group_field) return result + + +def parse_board(image: np.ndarray, conf: Configuration) -> Board: + result = Board() + result.field = parse_field(image, conf) + return result diff --git a/shenzhen_solitaire/card_detection/configuration.py b/shenzhen_solitaire/card_detection/configuration.py index b973c8b..1cb4da7 100644 --- a/shenzhen_solitaire/card_detection/configuration.py +++ b/shenzhen_solitaire/card_detection/configuration.py @@ -14,8 +14,12 @@ from . import card_finder from .. import board ADJUSTMENT_FILE_NAME = "adjustment.json" + FIELD_ADJUSTMENT_KEY = "field" BORDER_ADJUSTMENT_KEY = "border" +GOAL_ADJUSTMENT_KEY = "goal" +BUNKER_ADJUSTMENT_KEY = "bunker" +HUA_ADJUSTMENT_KEY = "hua" TEMPLATES_DIRECTORY = "templates" CARD_BORDER_DIRECTORY = "borders" @@ -28,11 +32,26 @@ PICTURE_EXTENSION = "png" class Configuration: """Configuration for solitaire cv""" - field_adjustment: adjustment.Adjustment - border_adjustment: adjustment.Adjustment - catalogue: List[Tuple[np.ndarray, Union[board.SpecialCard, board.NumberCard]]] - card_border: List[np.ndarray] - empty_card: List[np.ndarray] + field_adjustment: adjustment.Adjustment = dataclasses.field( + default_factory=adjustment.Adjustment + ) + border_adjustment: adjustment.Adjustment = dataclasses.field( + default_factory=adjustment.Adjustment + ) + goal_adjustment: adjustment.Adjustment = dataclasses.field( + default_factory=adjustment.Adjustment + ) + bunker_adjustment: adjustment.Adjustment = dataclasses.field( + default_factory=adjustment.Adjustment + ) + hua_adjustment: adjustment.Adjustment = dataclasses.field( + default_factory=adjustment.Adjustment + ) + catalogue: List[ + Tuple[np.ndarray, Union[board.SpecialCard, board.NumberCard]] + ] = dataclasses.field(default_factory=list) + card_border: List[np.ndarray] = dataclasses.field(default_factory=list) + empty_card: List[np.ndarray] = dataclasses.field(default_factory=list) meta: Dict[str, str] = dataclasses.field(default_factory=dict) @@ -55,10 +74,9 @@ def _save_catalogue( zip_file.write( myfile, arcname=f"{TEMPLATES_DIRECTORY}/{file_name}.{PICTURE_EXTENSION}" ) - -def _save_adjustments( - zip_file: zipfile.ZipFile, conf: Configuration -) -> None: + + +def _save_adjustments(zip_file: zipfile.ZipFile, conf: Configuration) -> None: adjustments = {} adjustments[FIELD_ADJUSTMENT_KEY] = dataclasses.asdict(conf.field_adjustment) adjustments[BORDER_ADJUSTMENT_KEY] = dataclasses.asdict(conf.border_adjustment) @@ -75,7 +93,7 @@ def save(conf: Configuration, filename: str) -> None: with zipfile.ZipFile(zip_stream, "w") as zip_file: _save_adjustments(zip_file, conf) _save_catalogue(zip_file, conf.catalogue) - + # TODO: Save card_borders and emtpy_card with open(filename, "wb") as zip_archive: zip_archive.write(zip_stream.getvalue()) @@ -98,7 +116,9 @@ def _load_catalogue(zip_file: zipfile.ZipFile,) -> List[Tuple[np.ndarray, board. mydir = tempfile.mkdtemp() for template_filename in ( - x for x in zip_file.namelist() if x.startswith(TEMPLATES_DIRECTORY + "/") + x + for x in zip_file.namelist() + if x.startswith(TEMPLATES_DIRECTORY + "/") and x != TEMPLATES_DIRECTORY + "/" ): myfile = zip_file.extract(template_filename, path=mydir) catalogue.append((cv2.imread(myfile), _parse_file_name(template_filename),)) @@ -111,7 +131,9 @@ def _load_dir(zip_file: zipfile.ZipFile, dirname: str) -> List[np.ndarray]: image_filenames = [ image_filename for image_filename in ( - x for x in zip_file.namelist() if x.startswith(dirname + "/") + x + for x in zip_file.namelist() + if x.startswith(dirname + "/") and x != dirname + "/" ) ] images = [ @@ -127,18 +149,28 @@ def load(filename: str) -> Configuration: with zipfile.ZipFile(filename, "r") as zip_file: adjustment_dict = json.loads(zip_file.read(ADJUSTMENT_FILE_NAME)) - return Configuration( + result = Configuration( field_adjustment=adjustment.Adjustment( - **adjustment_dict[FIELD_ADJUSTMENT_KEY] + **adjustment_dict.get(FIELD_ADJUSTMENT_KEY, {}) ), border_adjustment=adjustment.Adjustment( - **adjustment_dict[BORDER_ADJUSTMENT_KEY] + **adjustment_dict.get(BORDER_ADJUSTMENT_KEY, {}) + ), + goal_adjustment=adjustment.Adjustment( + **adjustment_dict.get(GOAL_ADJUSTMENT_KEY, {}) + ), + bunker_adjustment=adjustment.Adjustment( + **adjustment_dict.get(BUNKER_ADJUSTMENT_KEY, {}) + ), + hua_adjustment=adjustment.Adjustment( + **adjustment_dict.get(HUA_ADJUSTMENT_KEY, {}) ), catalogue=_load_catalogue(zip_file), card_border=_load_dir(zip_file, CARD_BORDER_DIRECTORY), empty_card=_load_dir(zip_file, EMPTY_CARD_DIRECTORY), meta={}, ) + return result def generate(image: np.ndarray) -> Configuration: diff --git a/test/test_cv.py b/test/test_cv.py index 7cccd86..b85307f 100644 --- a/test/test_cv.py +++ b/test/test_cv.py @@ -7,7 +7,7 @@ import numpy as np from shenzhen_solitaire import board from shenzhen_solitaire.card_detection import adjustment, board_parser -from shenzhen_solitaire.card_detection.configuration import Configuration +import shenzhen_solitaire.card_detection.configuration as configuration from . import boards @@ -16,10 +16,8 @@ class CardDetectionTest(unittest.TestCase): """Parse a configuration and a board""" image = cv2.imread("pictures/20190809172206_1.jpg") - loaded_config = Configuration.load("test_config.zip") + loaded_config = configuration.load("test_config.zip") my_board = board_parser.parse_board(image, loaded_config) - for rows in zip(boards.B20190809172206_1.field, my_board.field): - for good_cell, test_cell in zip(*rows): - self.assertEqual(good_cell, test_cell) - + for correct_row, my_row in zip(boards.B20190809172206_1.field, my_board.field): + self.assertListEqual(correct_row, my_row) diff --git a/test_config.zip b/test_config.zip index 8e6c0bd77a3d919535377b61a40a8fbc2569d79c..f3b6a3e702c493937c08972ee35a6f3fe51c6c2c 100644 GIT binary patch delta 6886 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 delta 3373 zcmZuyZA@Eb6u$ic`4}QnxGlp3C?pV8=xqlKCbA)$I%L)kk+|VRsDlZG31ko@Sd(oI z=Z0J}$e1G|O+@_xSobAkD$bZK#+aZ{r;f}_T_QS|@?-O3p7Y-J?fc$q$!TBiJ}c~opV6IB zl2+ujuS(14fm1=zYB=qNlT-|cCO9}GI5eW0RN%lP`Z=Xul1ENx5LXI9r>C{OrM}bC zk?)n9wg;Z`c)T_#uid*t)|qkx{SDU*<}juGyPNBMMnOPJ66CrwB6wK?1MN>6btbv0 zM;%ZK26P4kW(Gd!X)=Io->4|J%_!)F(F&s_oa68tPrPh}_-JN9kk_S}kSH_I zxz&J)E@z7JnRi!`ORlUr3|MDV<#tH6@|OCBXKh8n+}6P7wjcH2I`zJ&6Xa=MN?^%% z6|qaDDy&ox`jWWQ|G*bCD!>y%&D9S+;ZdYe=j1Y6~S_L#tMaV=Xx zQRy%LBtwyCWv2cIc-|pznf!2DQlN(v-xO^z*FS+=d&C--8Pp&OzU} zg>>zk@w3QJ@{97tnN)Jg@1Mm>Q+BN5f?WxhDGjmVs>F}hH3*{5uy*B?^yt1Xi(|x(@L$A%trbL@#2T4L>L)UM0jMc zZO_IVR|et6S%plse2VQ!&ECa*i9iGmi`8vbLT=mq2O{exv7g6c%+O?OU^%9mZB+D{ zHsL3rhlbdDa}?}Q&L@cdV*3Ba#~GSEYvAa?3U+i}z5AsVNk9V)`jRlCG<5&SGDbT- zflQ`!N*iMI@htT9sNoW>S&8!GR3sUU1LhI7t#2QhuRaZY(NZ8@uq%B-!)#YtzgxVt z9z4)+@VuLpw4ZKDXM-)8zWO>e>=3MUDm9=pnT_Q-y~z;XX6hvldz^e;)Zve6rMoH# z7&r4{iW`3WeNFHkeiK!DItU{EC>#8aCYQ;V-*>`@8~ct>7FgGI_I7hLpM_iAArLI? zJ6TY69PV+OnuKbJQ-t4lE`hFe$^`Z~7gS8f^-gcYZs&-uhk0Y#!EgEyQP}b*n!%6X z*K-fq`7|gj{hH8S?D0T47nGL(b$P1wPviJ~Z)FiNa1b$2!&E}Rz?O0lma4*9H8t%ECy(~1xVbb`YiD&#S&YJ@PL7+iDbONf5Z=7M;*SsqDc zY)n>4)CKbr!p69GKMIOw(o||f5q@9tgG5_22$irf2*<-jW@s6pG2C2MxKMKFZO|L8 z3r|)P_0c3$Ne`2-suo`cZBZ%}Vh|JCh$d+Ug%>0WfI<1~DU>Hl8nlV4Dc*xprHRMu z6dP|#Gpd#bz)Kbj7;A+|wNL_}UR>7(cR>&O!*4qX8AtGiF4VOZMAt&|2b#wD-0Vh) z(==YCZ None: @@ -15,9 +19,15 @@ def main() -> None: image, count_x=1, count_y=1, adjustment=copy.deepcopy(border_adjustment) ) border_square = card_finder.get_field_squares(image, border_square_pos, 1, 1) - empty_square = card_finder.get_field_squares(image, border_square_pos, 1, 1) + empty_square_pos = adjustment.adjust_squares( + image, count_x=1, count_y=1, adjustment=copy.deepcopy(border_adjustment) + ) + empty_square = card_finder.get_field_squares(image, empty_square_pos, 1, 1) + + cv2.imwrite("/tmp/border_square.png", border_square[0]) + cv2.imwrite("/tmp/empty_square.png", empty_square[0]) + print(json.dumps(dataclasses.asdict(border_adjustment))) if __name__ == "__main__": main() -