Made border detection work

This commit is contained in:
Lukas Wölfer
2020-02-08 00:42:55 +01:00
parent cf89e4c694
commit 07f8cae238
7 changed files with 112 additions and 61 deletions

View File

@@ -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 # 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_ROW_SIZE = 13
MAX_COLUMN_SIZE = 8
def __init__(self) -> None: 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.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3
self.goal: Dict[NumberCard.Suit, int] = { self.goal: Dict[NumberCard.Suit, int] = {
NumberCard.Suit.Red: 0, NumberCard.Suit.Red: 0,

View File

@@ -10,31 +10,36 @@ import cv2
@dataclass @dataclass
class Adjustment: class Adjustment:
"""Configuration for a grid""" """Configuration for a grid"""
x: int
y: int x: int = 0
w: int y: int = 0
h: int w: int = 0
dx: int h: int = 0
dy: int dx: int = 0
dy: int = 0
def get_square(adjustment: Adjustment, index_x: int = 0, def get_square(
index_y: int = 0) -> Tuple[int, int, int, int]: adjustment: Adjustment, index_x: int = 0, index_y: int = 0
) -> Tuple[int, int, int, int]:
"""Get one square from index and adjustment""" """Get one square from index and adjustment"""
return (adjustment.x + adjustment.dx * index_x, return (
adjustment.x + adjustment.dx * index_x,
adjustment.y + adjustment.dy * index_y, adjustment.y + adjustment.dy * index_y,
adjustment.x + adjustment.w + adjustment.dx * index_x, adjustment.x + adjustment.w + adjustment.dx * index_x,
adjustment.y + adjustment.h + adjustment.dy * index_y) adjustment.y + adjustment.h + adjustment.dy * index_y,
)
def adjust_squares( def adjust_squares(
image: numpy.ndarray, image: numpy.ndarray,
count_x: int, count_x: int,
count_y: int, count_y: int,
adjustment: Optional[Adjustment] = None) -> Adjustment: adjustment: Optional[Adjustment] = None,
) -> Adjustment:
if not adjustment: if not adjustment:
adjustment = Adjustment(0, 0, 0, 0, 0, 0) adjustment = Adjustment(w=10, h=10)
def _adjustment_step(keycode: int) -> None: def _adjustment_step(keycode: int) -> None:
assert adjustment is not None assert adjustment is not None
@@ -59,21 +64,19 @@ def adjust_squares(
while True: while True:
working_image = image.copy() working_image = image.copy()
for index_x, index_y in itertools.product( for index_x, index_y in itertools.product(range(count_x), range(count_y)):
range(count_x), range(count_y)):
square = get_square(adjustment, index_x, index_y) square = get_square(adjustment, index_x, index_y)
cv2.rectangle(working_image, cv2.rectangle(
(square[0], square[1]), working_image, (square[0], square[1]), (square[2], square[3]), (0, 0, 0)
(square[2], square[3]), )
(0, 0, 0)) cv2.imshow("Window", working_image)
cv2.imshow('Window', working_image)
keycode = cv2.waitKey(0) keycode = cv2.waitKey(0)
print(keycode) print(keycode)
if keycode == 27: if keycode == 27:
break break
_adjustment_step(keycode) _adjustment_step(keycode)
cv2.destroyWindow('Window') cv2.destroyWindow("Window")
return adjustment return adjustment

View File

@@ -2,7 +2,7 @@
import numpy as np import numpy as np
from .configuration import Configuration from .configuration import Configuration
from ..board import Board, NumberCard, SpecialCard from ..board import Board, NumberCard, SpecialCard, Card
from . import card_finder from . import card_finder
import cv2 import cv2
from typing import Iterable, Any, List, Tuple, Union from typing import Iterable, Any, List, Tuple, Union
@@ -37,12 +37,13 @@ def get_square_iterator(
return zip(grouped_squares, grouped_border_squares) 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""" """Return matchiness for the template on the search image"""
res = cv2.matchTemplate(search_image, template, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(search_image, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
assert isinstance(max_val, int) assert isinstance(max_val, (int, float))
return max_val return float(max_val)
def parse_square( def parse_square(
@@ -70,13 +71,13 @@ def parse_square(
return (best_name, row_finished) 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""" """Parse a screenshot of the game, using a given configuration"""
square_iterator = get_square_iterator( 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() result = []
for group_index, (square_group, border_group) in enumerate(square_iterator): for square_group, border_group in square_iterator:
group_field = [] group_field = []
for index, (square, border_square) in enumerate( for index, (square, border_square) in enumerate(
zip(square_group, border_group) zip(square_group, border_group)
@@ -86,6 +87,12 @@ def parse_board(image: np.ndarray, conf: Configuration) -> Board:
if row_finished: if row_finished:
break break
result.field[group_index] = group_field result.append(group_field)
return result return result
def parse_board(image: np.ndarray, conf: Configuration) -> Board:
result = Board()
result.field = parse_field(image, conf)
return result

View File

@@ -14,8 +14,12 @@ from . import card_finder
from .. import board from .. import board
ADJUSTMENT_FILE_NAME = "adjustment.json" ADJUSTMENT_FILE_NAME = "adjustment.json"
FIELD_ADJUSTMENT_KEY = "field" FIELD_ADJUSTMENT_KEY = "field"
BORDER_ADJUSTMENT_KEY = "border" BORDER_ADJUSTMENT_KEY = "border"
GOAL_ADJUSTMENT_KEY = "goal"
BUNKER_ADJUSTMENT_KEY = "bunker"
HUA_ADJUSTMENT_KEY = "hua"
TEMPLATES_DIRECTORY = "templates" TEMPLATES_DIRECTORY = "templates"
CARD_BORDER_DIRECTORY = "borders" CARD_BORDER_DIRECTORY = "borders"
@@ -28,11 +32,26 @@ PICTURE_EXTENSION = "png"
class Configuration: class Configuration:
"""Configuration for solitaire cv""" """Configuration for solitaire cv"""
field_adjustment: adjustment.Adjustment field_adjustment: adjustment.Adjustment = dataclasses.field(
border_adjustment: adjustment.Adjustment default_factory=adjustment.Adjustment
catalogue: List[Tuple[np.ndarray, Union[board.SpecialCard, board.NumberCard]]] )
card_border: List[np.ndarray] border_adjustment: adjustment.Adjustment = dataclasses.field(
empty_card: List[np.ndarray] 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) meta: Dict[str, str] = dataclasses.field(default_factory=dict)
@@ -56,9 +75,8 @@ def _save_catalogue(
myfile, arcname=f"{TEMPLATES_DIRECTORY}/{file_name}.{PICTURE_EXTENSION}" myfile, arcname=f"{TEMPLATES_DIRECTORY}/{file_name}.{PICTURE_EXTENSION}"
) )
def _save_adjustments(
zip_file: zipfile.ZipFile, conf: Configuration def _save_adjustments(zip_file: zipfile.ZipFile, conf: Configuration) -> None:
) -> None:
adjustments = {} adjustments = {}
adjustments[FIELD_ADJUSTMENT_KEY] = dataclasses.asdict(conf.field_adjustment) adjustments[FIELD_ADJUSTMENT_KEY] = dataclasses.asdict(conf.field_adjustment)
adjustments[BORDER_ADJUSTMENT_KEY] = dataclasses.asdict(conf.border_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: with zipfile.ZipFile(zip_stream, "w") as zip_file:
_save_adjustments(zip_file, conf) _save_adjustments(zip_file, conf)
_save_catalogue(zip_file, conf.catalogue) _save_catalogue(zip_file, conf.catalogue)
# TODO: Save card_borders and emtpy_card
with open(filename, "wb") as zip_archive: with open(filename, "wb") as zip_archive:
zip_archive.write(zip_stream.getvalue()) 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() mydir = tempfile.mkdtemp()
for template_filename in ( 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) myfile = zip_file.extract(template_filename, path=mydir)
catalogue.append((cv2.imread(myfile), _parse_file_name(template_filename),)) 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_filenames = [
image_filename image_filename
for image_filename in ( 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 = [ images = [
@@ -127,18 +149,28 @@ def load(filename: str) -> Configuration:
with zipfile.ZipFile(filename, "r") as zip_file: with zipfile.ZipFile(filename, "r") as zip_file:
adjustment_dict = json.loads(zip_file.read(ADJUSTMENT_FILE_NAME)) adjustment_dict = json.loads(zip_file.read(ADJUSTMENT_FILE_NAME))
return Configuration( result = Configuration(
field_adjustment=adjustment.Adjustment( field_adjustment=adjustment.Adjustment(
**adjustment_dict[FIELD_ADJUSTMENT_KEY] **adjustment_dict.get(FIELD_ADJUSTMENT_KEY, {})
), ),
border_adjustment=adjustment.Adjustment( 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), catalogue=_load_catalogue(zip_file),
card_border=_load_dir(zip_file, CARD_BORDER_DIRECTORY), card_border=_load_dir(zip_file, CARD_BORDER_DIRECTORY),
empty_card=_load_dir(zip_file, EMPTY_CARD_DIRECTORY), empty_card=_load_dir(zip_file, EMPTY_CARD_DIRECTORY),
meta={}, meta={},
) )
return result
def generate(image: np.ndarray) -> Configuration: def generate(image: np.ndarray) -> Configuration:

View File

@@ -7,7 +7,7 @@ import numpy as np
from shenzhen_solitaire import board from shenzhen_solitaire import board
from shenzhen_solitaire.card_detection import adjustment, board_parser 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 from . import boards
@@ -16,10 +16,8 @@ class CardDetectionTest(unittest.TestCase):
"""Parse a configuration and a board""" """Parse a configuration and a board"""
image = cv2.imread("pictures/20190809172206_1.jpg") 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) my_board = board_parser.parse_board(image, loaded_config)
for rows in zip(boards.B20190809172206_1.field, my_board.field): for correct_row, my_row in zip(boards.B20190809172206_1.field, my_board.field):
for good_cell, test_cell in zip(*rows): self.assertListEqual(correct_row, my_row)
self.assertEqual(good_cell, test_cell)

Binary file not shown.

View File

@@ -1,9 +1,13 @@
import numpy as np import copy
import dataclasses
import json
import cv2 import cv2
from shenzhen_solitaire.card_detection.configuration import Configuration import numpy as np
import shenzhen_solitaire.card_detection.adjustment as adjustment import shenzhen_solitaire.card_detection.adjustment as adjustment
import shenzhen_solitaire.card_detection.card_finder as card_finder import shenzhen_solitaire.card_detection.card_finder as card_finder
import copy from shenzhen_solitaire.card_detection.configuration import Configuration
def main() -> None: def main() -> None:
@@ -15,9 +19,15 @@ def main() -> None:
image, count_x=1, count_y=1, adjustment=copy.deepcopy(border_adjustment) 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) 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__": if __name__ == "__main__":
main() main()