From 26b4f6bf3da1db3a74bfa4c905d1fb7b7b2bcf8f Mon Sep 17 00:00:00 2001 From: Florian Vahl <7vahl@informatik.uni-hamburg.de> Date: Thu, 21 Dec 2023 14:35:26 +0100 Subject: [PATCH 1/8] Move map generator --- soccer_field_map_generator/package.xml | 25 + .../resource/soccer_field_map_generator | 0 soccer_field_map_generator/setup.cfg | 4 + soccer_field_map_generator/setup.py | 27 + .../soccer_field_map_generator/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 188 bytes .../__pycache__/generator.cpython-310.pyc | Bin 0 -> 8198 bytes .../__pycache__/gui.cpython-310.pyc | Bin 0 -> 9345 bytes .../__pycache__/tooltip.cpython-310.pyc | Bin 0 -> 3960 bytes .../soccer_field_map_generator/cli.py | 67 ++ .../soccer_field_map_generator/generator.py | 695 ++++++++++++++++++ .../soccer_field_map_generator/gui.py | 407 ++++++++++ .../soccer_field_map_generator/tooltip.py | 151 ++++ .../test/test_copyright.py | 25 + .../test/test_flake8.py | 25 + .../test/test_pep257.py | 23 + 16 files changed, 1449 insertions(+) create mode 100644 soccer_field_map_generator/package.xml create mode 100644 soccer_field_map_generator/resource/soccer_field_map_generator create mode 100644 soccer_field_map_generator/setup.cfg create mode 100644 soccer_field_map_generator/setup.py create mode 100644 soccer_field_map_generator/soccer_field_map_generator/__init__.py create mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/__init__.cpython-310.pyc create mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/generator.cpython-310.pyc create mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/gui.cpython-310.pyc create mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/tooltip.cpython-310.pyc create mode 100644 soccer_field_map_generator/soccer_field_map_generator/cli.py create mode 100755 soccer_field_map_generator/soccer_field_map_generator/generator.py create mode 100644 soccer_field_map_generator/soccer_field_map_generator/gui.py create mode 100644 soccer_field_map_generator/soccer_field_map_generator/tooltip.py create mode 100644 soccer_field_map_generator/test/test_copyright.py create mode 100644 soccer_field_map_generator/test/test_flake8.py create mode 100644 soccer_field_map_generator/test/test_pep257.py diff --git a/soccer_field_map_generator/package.xml b/soccer_field_map_generator/package.xml new file mode 100644 index 0000000..c513a91 --- /dev/null +++ b/soccer_field_map_generator/package.xml @@ -0,0 +1,25 @@ + + + + soccer_field_map_generator + 0.0.0 + A package to generate a soccer field map using a gui or cli + Florian Vahl + Apache License 2.0 + + python3-opencv + python3-numpy + python3-tk + python3-yaml + python3-pil + python3-scipy + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/soccer_field_map_generator/resource/soccer_field_map_generator b/soccer_field_map_generator/resource/soccer_field_map_generator new file mode 100644 index 0000000..e69de29 diff --git a/soccer_field_map_generator/setup.cfg b/soccer_field_map_generator/setup.cfg new file mode 100644 index 0000000..4ff91cd --- /dev/null +++ b/soccer_field_map_generator/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/soccer_field_map_generator +[install] +install_scripts=$base/lib/soccer_field_map_generator diff --git a/soccer_field_map_generator/setup.py b/soccer_field_map_generator/setup.py new file mode 100644 index 0000000..58f0679 --- /dev/null +++ b/soccer_field_map_generator/setup.py @@ -0,0 +1,27 @@ +from setuptools import find_packages, setup + +package_name = 'soccer_field_map_generator' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='florian', + maintainer_email='git@flova.de', + description='A package to generate a soccer field map using a gui or cli', + license='Apache License 2.0', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'gui = soccer_field_map_generator.gui:main', + 'cli = soccer_field_map_generator.cli:main', + ], + }, +) diff --git a/soccer_field_map_generator/soccer_field_map_generator/__init__.py b/soccer_field_map_generator/soccer_field_map_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/soccer_field_map_generator/soccer_field_map_generator/__pycache__/__init__.cpython-310.pyc b/soccer_field_map_generator/soccer_field_map_generator/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e3e9c8e732c9ffba78924850fd83d05115564d3 GIT binary patch literal 188 zcmd1j<>g`k0;8WzsUZ3>h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o10gKO;XkwOHTK zyeu&zM?X0~CpkYazPwmJsWdYuMZY*dIXSf`J}omfCnY{Nu^>J@H7~U&u_V6;pHzH& fW?p7Ve7s&k^`TZ-~`szm=RR9?le_eT(>Ftw>v0 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 literal 0 HcmV?d00001 diff --git a/soccer_field_map_generator/soccer_field_map_generator/__pycache__/tooltip.cpython-310.pyc b/soccer_field_map_generator/soccer_field_map_generator/__pycache__/tooltip.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3396f80a12e88bd0464f01c4f328df77035969f6 GIT binary patch literal 3960 zcmaJ^&2QVt73Y^o>ch6;>^jc21=vjstlCJjosTwY5F~AqBHPqiAnq10iV_rOB+;fw zWk^|;L0!7J1}M<$9_(ZOkKTLdwI}}vyL)K+dqYWT~Yo?gXKrZ;0apt2?$e|8Y=thZxyC7{hh+}ue5!Qsc)2qagOiGu1|FNF)?_8 zmh6LYtYr$V*w;w{Ggyh4%=)^-Y*v1!?HlX{tFqcVb-#q(3R^|bWNWPcPT9BEHMWko z&91YL@Gi5D*#_PfR>A(8xwRL^VHym*e*kmE!Sgb8e8IhxCywW&#aE{r3ygZf5s#d4 zziLHq59 zCutBzN&C*u&h~?Q_uFzMVZbx)ducOBn%JY61cPCS$<47hX{K>=;2m0uD1T(s}pht!?z`?uzuOK`^Haf^9TFyvG@CJh*>a~-&n~pufn~WdvFdhzg zlwQK-$Beh{{_^g<2it9L4y5UaUXnDSsX2sV!>03E1ic{g!f@h@MUbY@VQ1nT@t&7J z&!o1Ut?j$5tq1fsU-bK!1>Jx%n7|1lKO8X*3ossf6DJJ@oJ4#X_$dst@A$3Z8Yd*(1s0`f8#_R1=cF~=VzIAd_)(Si;Jn3n=?bcm|KWg@LxWTU&DjInEwj8-|92 zKXgV>I&!vQ!nq!XQG!lj*iv#-_=F&r=EHsk)8?=OhWTi}TmCOWHGZj?J3{X8&4zyE)fpbtg(^O?P*6=o$ z#x$=Ohh0&lFN@Do%7yR`inxyN z-~GAWk717X_AidSe%OW|`ElfqlXhnmgbb<0_qlKp*dcQV-q7tKT!e=x{(qllanu@4 za@%!-C`euR7IsTEK+KA%(yuOmMZcn+)c1<~*qSfcU04i@6tW&QX)!ukrkpBgii&FW zM){!bHq_is{61%+kbls`I%ec01a_3la~9+yaK$Rf1GI$1)zp)<`NlKV{_kyR`FvZv z)%o^WREGW3NcB@A zGtMX-AUzzEPEBUgDN_BwqH(4%i`nn>X$c&gQpiCWy-KEmRvFEyvGRLsYG&raN~SaG zrUF{IsdP2Gt8!GM5$3P5mG?RlOzCum{0d3P$jq;_KWS5IYRlc%*lK2Fc30a_=DKXG z?KZS?nK8&m1Q`O3SjGj(Md?jFnHd%yO}3(_r@3#?l5apVWr|Z$FoD9SHEmh~Vx6hL z2g{u)Hwd+Mvv}*0tIxOwaj}9h8mGpY0=(sEB`ZTWSdW#^2E!UTt2(V^)yx97mf4cC zGF{D9veKq9UCY*(dZ>ucnTq~uW`bAG>XNqyUOijO>RpYM4)y7^?3x&6Yoa^fXH&^6 z%&%s3@Mk-MXM}$k!HK*Xwyf zbS1{}b@CrvL=cXlKME<`{|*3?Ef7Op*EaMG)%;hj^oh2v+M10JwDB|4nr3J+9JM<5 z=v%gS^4UBvDf6@*g)xLCk6Yj0Voxfb^616ICa-p4fkY~g-!O|(XdU1d6m%!zA@Jm; z;92>sK$I6(!v0VNGuA zzIpz~yb38jj%u5VNuyjOUn)db0qF1@(V%_R<6NCwF^>2VzI=|BdbiNwbq6t3a`bDi`*!4o zvoDtGve*}rGvDaMld-{j`@qXO__N*hJ#RMz$X05gEiY5)KL literal 0 HcmV?d00001 diff --git a/soccer_field_map_generator/soccer_field_map_generator/cli.py b/soccer_field_map_generator/soccer_field_map_generator/cli.py new file mode 100644 index 0000000..d3f5166 --- /dev/null +++ b/soccer_field_map_generator/soccer_field_map_generator/cli.py @@ -0,0 +1,67 @@ +import sys +import os +import cv2 +import yaml +import argparse + +from soccer_field_map_generator.generator import ( + generate_map_image, + generate_metadata, + load_config_file, +) + + +def main(): + parser = argparse.ArgumentParser(description="Generate maps for localization") + parser.add_argument("output", help="Output file name") + parser.add_argument( + "config", + help="Config file for the generator that specifies the parameters for the map generation", + ) + parser.add_argument( + "--metadata", + help="Also generates a 'map_server.yaml' file with the metadata for the map", + action="store_true", + ) + args = parser.parse_args() + + # Check if the config file exists + if not os.path.isfile(args.config): + print("Config file does not exist") + sys.exit(1) + + # Load config file + with open(args.config, "r") as config_file: + parameters = load_config_file(config_file) + + # Check if the config file is valid + if parameters is None: + print("Invalid config file") + sys.exit(1) + + # Generate the map image + image = generate_map_image(parameters) + + # Make output folder full path + output_path = os.path.abspath(args.output) + + # Check if the output folder exists + if not os.path.isdir(os.path.dirname(output_path)): + print("Output folder does not exist") + sys.exit(1) + + # Save the image + cv2.imwrite(output_path, image) + + # Generate the metadata + if args.metadata: + metadata = generate_metadata(parameters, os.path.basename(output_path)) + metadata_file_name = os.path.join( + os.path.dirname(output_path), "map_server.yaml" + ) + with open(metadata_file_name, "w") as metadata_file: + yaml.dump(metadata, metadata_file, sort_keys=False) + + +if __name__ == "__main__": + main() diff --git a/soccer_field_map_generator/soccer_field_map_generator/generator.py b/soccer_field_map_generator/soccer_field_map_generator/generator.py new file mode 100755 index 0000000..c3476d2 --- /dev/null +++ b/soccer_field_map_generator/soccer_field_map_generator/generator.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python3 + +import math +import yaml + +import cv2 +import numpy as np +from typing import Optional +from scipy import ndimage +from enum import Enum + + +class MapTypes(Enum): + LINE = "line" + POSTS = "posts" + FIELD_BOUNDARY = "field_boundary" + FIELD_FEATURES = "field_features" + CORNERS = "corners" + TCROSSINGS = "tcrossings" + CROSSES = "crosses" + + +class MarkTypes(Enum): + POINT = "point" + CROSS = "cross" + + +class FieldFeatureStyles(Enum): + EXACT = "exact" + BLOB = "blob" + + +def drawCross(img, point, color, width=5, length=15): + vertical_start = (point[0], point[1] - length) + vertical_end = (point[0], point[1] + length) + horizontal_start = (point[0] - length, point[1]) + horizontal_end = (point[0] + length, point[1]) + img = cv2.line(img, vertical_start, vertical_end, color, width) + img = cv2.line(img, horizontal_start, horizontal_end, color, width) + + +def drawDistance(image, decay_factor): + # Calc distances + distance_map = ndimage.distance_transform_edt(255 - image) + + # Activation function that leads to a faster decay at the beginning + # and a slower decay at the end and is parameterized by the decay factor + if not math.isclose(decay_factor, 0): + distance_map = np.exp(-distance_map * decay_factor * 0.1) + else: + distance_map = -distance_map + + # Scale to bounds of uint8 (0-255) + return cv2.normalize( + distance_map, + None, + alpha=0, + beta=255, + norm_type=cv2.NORM_MINMAX, + dtype=cv2.CV_64F, + ).astype(np.uint8) + + +def generate_map_image(parameters): + target = MapTypes(parameters["map_type"]) + mark_type = MarkTypes(parameters["mark_type"]) + field_feature_style = FieldFeatureStyles(parameters["field_feature_style"]) + + penalty_mark = parameters["penalty_mark"] + center_point = parameters["center_point"] + goal_back = parameters["goal_back"] # Draw goal back area + + stroke_width = parameters["stroke_width"] + field_length = parameters["field_length"] + field_width = parameters["field_width"] + goal_depth = parameters["goal_depth"] + goal_width = parameters["goal_width"] + goal_area_length = parameters["goal_area_length"] + goal_area_width = parameters["goal_area_width"] + penalty_mark_distance = parameters["penalty_mark_distance"] + center_circle_diameter = parameters["center_circle_diameter"] + border_strip_width = parameters["border_strip_width"] + penalty_area_length = parameters["penalty_area_length"] + penalty_area_width = parameters["penalty_area_width"] + field_feature_size = parameters["field_feature_size"] + distance_map = parameters["distance_map"] + distance_decay = parameters["distance_decay"] + invert = parameters["invert"] + + # Color of the lines and marks + color = (255, 255, 255) # white + + # Size of complete turf field (field with outside borders) + image_size = ( + field_width + border_strip_width * 2, + field_length + border_strip_width * 2, + 3, + ) + + # Calculate important points on the field + field_outline_start = (border_strip_width, border_strip_width) + field_outline_end = ( + field_length + border_strip_width, + field_width + border_strip_width, + ) + + middle_line_start = (field_length // 2 + border_strip_width, border_strip_width) + middle_line_end = ( + field_length // 2 + border_strip_width, + field_width + border_strip_width, + ) + + middle_point = ( + field_length // 2 + border_strip_width, + field_width // 2 + border_strip_width, + ) + + penalty_mark_left = ( + penalty_mark_distance + border_strip_width, + field_width // 2 + border_strip_width, + ) + penalty_mark_right = ( + image_size[1] - border_strip_width - penalty_mark_distance, + field_width // 2 + border_strip_width, + ) + + goal_area_left_start = ( + border_strip_width, + border_strip_width + field_width // 2 - goal_area_width // 2, + ) + goal_area_left_end = ( + border_strip_width + goal_area_length, + field_width // 2 + border_strip_width + goal_area_width // 2, + ) + + goal_area_right_start = ( + image_size[1] - goal_area_left_start[0], + goal_area_left_start[1], + ) + goal_area_right_end = (image_size[1] - goal_area_left_end[0], goal_area_left_end[1]) + + penalty_area_left_start = ( + border_strip_width, + border_strip_width + field_width // 2 - penalty_area_width // 2, + ) + penalty_area_left_end = ( + border_strip_width + penalty_area_length, + field_width // 2 + border_strip_width + penalty_area_width // 2, + ) + + penalty_area_right_start = ( + image_size[1] - penalty_area_left_start[0], + penalty_area_left_start[1], + ) + penalty_area_right_end = ( + image_size[1] - penalty_area_left_end[0], + penalty_area_left_end[1], + ) + + goalpost_left_1 = ( + border_strip_width, + border_strip_width + field_width // 2 + goal_width // 2, + ) + goalpost_left_2 = ( + border_strip_width, + border_strip_width + field_width // 2 - goal_width // 2, + ) + + goalpost_right_1 = (image_size[1] - goalpost_left_1[0], goalpost_left_1[1]) + goalpost_right_2 = (image_size[1] - goalpost_left_2[0], goalpost_left_2[1]) + + goal_back_corner_left_1 = (goalpost_left_1[0] - goal_depth, goalpost_left_1[1]) + goal_back_corner_left_2 = (goalpost_left_2[0] - goal_depth, goalpost_left_2[1]) + + goal_back_corner_right_1 = (goalpost_right_1[0] + goal_depth, goalpost_right_1[1]) + goal_back_corner_right_2 = (goalpost_right_2[0] + goal_depth, goalpost_right_2[1]) + + # Create black image in the correct size + img = np.zeros(image_size, np.uint8) + + # Check which map type we want to generate + if target == MapTypes.LINE: + # Draw outline + img = cv2.rectangle( + img, field_outline_start, field_outline_end, color, stroke_width + ) + + # Draw middle line + img = cv2.line(img, middle_line_start, middle_line_end, color, stroke_width) + + # Draw center circle + img = cv2.circle( + img, middle_point, center_circle_diameter // 2, color, stroke_width + ) + + # Draw center mark (point or cross) + if center_point: + if mark_type == MarkTypes.POINT: + img = cv2.circle(img, middle_point, stroke_width * 2, color, -1) + elif mark_type == MarkTypes.CROSS: + drawCross(img, middle_point, color, stroke_width) + else: + raise NotImplementedError("Mark type not implemented") + + # Draw penalty marks (point or cross) + if penalty_mark: + if mark_type == MarkTypes.POINT: + img = cv2.circle(img, penalty_mark_left, stroke_width * 2, color, -1) + img = cv2.circle(img, penalty_mark_right, stroke_width * 2, color, -1) + elif mark_type == MarkTypes.CROSS: + drawCross(img, penalty_mark_left, color, stroke_width) + drawCross(img, penalty_mark_right, color, stroke_width) + else: + raise NotImplementedError("Mark type not implemented") + + # Draw goal area + img = cv2.rectangle( + img, goal_area_left_start, goal_area_left_end, color, stroke_width + ) + img = cv2.rectangle( + img, goal_area_right_start, goal_area_right_end, color, stroke_width + ) + + # Draw penalty area + img = cv2.rectangle( + img, penalty_area_left_start, penalty_area_left_end, color, stroke_width + ) + img = cv2.rectangle( + img, penalty_area_right_start, penalty_area_right_end, color, stroke_width + ) + + # Draw goal back area + if goal_back: + img = cv2.rectangle( + img, goalpost_left_1, goal_back_corner_left_2, color, stroke_width + ) + img = cv2.rectangle( + img, goalpost_right_1, goal_back_corner_right_2, color, stroke_width + ) + + if target == MapTypes.POSTS: + # Draw goalposts + img = cv2.circle(img, goalpost_left_1, stroke_width * 2, color, -1) + img = cv2.circle(img, goalpost_left_2, stroke_width * 2, color, -1) + img = cv2.circle(img, goalpost_right_1, stroke_width * 2, color, -1) + img = cv2.circle(img, goalpost_right_2, stroke_width * 2, color, -1) + + if target == MapTypes.FIELD_BOUNDARY: + # We need a larger image for this as we draw outside the field + img = np.zeros((image_size[0] + 200, image_size[1] + 200), np.uint8) + + # Draw fieldboundary + img = cv2.rectangle( + img, + (100, 100), + (image_size[1] + 100, image_size[0] + 100), + color, + stroke_width, + ) + + if ( + target in [MapTypes.CORNERS, MapTypes.FIELD_FEATURES] + and field_feature_style == FieldFeatureStyles.EXACT + ): + # draw outline corners + # top left + img = cv2.line( + img, + field_outline_start, + (field_outline_start[0], field_outline_start[1] + field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + field_outline_start, + (field_outline_start[0] + field_feature_size, field_outline_start[1]), + color, + stroke_width, + ) + + # bottom left + img = cv2.line( + img, + (field_outline_start[0], field_outline_end[1]), + (field_outline_start[0], field_outline_end[1] - field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + (field_outline_start[0], field_outline_end[1]), + (field_outline_start[0] + field_feature_size, field_outline_end[1]), + color, + stroke_width, + ) + + # top right + img = cv2.line( + img, + (field_outline_end[0], field_outline_start[1]), + (field_outline_end[0], field_outline_start[1] + field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + (field_outline_end[0], field_outline_start[1]), + (field_outline_end[0] - field_feature_size, field_outline_start[1]), + color, + stroke_width, + ) + + # bottom right + img = cv2.line( + img, + field_outline_end, + (field_outline_end[0], field_outline_end[1] - field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + field_outline_end, + (field_outline_end[0] - field_feature_size, field_outline_end[1]), + color, + stroke_width, + ) + + # draw left goal area corners + # top + img = cv2.line( + img, + (goal_area_left_end[0], goal_area_left_start[1]), + (goal_area_left_end[0], goal_area_left_start[1] + field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + (goal_area_left_end[0], goal_area_left_start[1]), + (goal_area_left_end[0] - field_feature_size, goal_area_left_start[1]), + color, + stroke_width, + ) + + # bottom + + img = cv2.line( + img, + goal_area_left_end, + (goal_area_left_end[0], goal_area_left_end[1] - field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + goal_area_left_end, + (goal_area_left_end[0] - field_feature_size, goal_area_left_end[1]), + color, + stroke_width, + ) + + # draw right goal aera corners + + # top + + img = cv2.line( + img, + (goal_area_right_end[0], goal_area_right_start[1]), + (goal_area_right_end[0], goal_area_right_start[1] + field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + (goal_area_right_end[0], goal_area_right_start[1]), + (goal_area_right_end[0] + field_feature_size, goal_area_right_start[1]), + color, + stroke_width, + ) + + # bottom + + img = cv2.line( + img, + goal_area_right_end, + (goal_area_right_end[0], goal_area_right_end[1] - field_feature_size), + color, + stroke_width, + ) + img = cv2.line( + img, + goal_area_right_end, + (goal_area_right_end[0] + field_feature_size, goal_area_right_end[1]), + color, + stroke_width, + ) + + if ( + target in [MapTypes.CORNERS, MapTypes.FIELD_FEATURES] + and field_feature_style == FieldFeatureStyles.BLOB + ): + # field corners + img = cv2.circle(img, field_outline_start, field_feature_size, color, -1) + img = cv2.circle( + img, + (field_outline_start[0], field_outline_end[1]), + field_feature_size, + color, + -1, + ) + img = cv2.circle( + img, + (field_outline_end[0], field_outline_start[1]), + field_feature_size, + color, + -1, + ) + img = cv2.circle(img, field_outline_end, field_feature_size, color, -1) + + # goal area corners + img = cv2.circle( + img, + (goal_area_left_end[0], goal_area_left_start[1]), + field_feature_size, + color, + -1, + ) + img = cv2.circle(img, goal_area_left_end, field_feature_size, color, -1) + img = cv2.circle( + img, + (goal_area_right_end[0], goal_area_right_start[1]), + field_feature_size, + color, + -1, + ) + img = cv2.circle(img, goal_area_right_end, field_feature_size, color, -1) + + if ( + target in [MapTypes.TCROSSINGS, MapTypes.FIELD_FEATURES] + and field_feature_style == FieldFeatureStyles.EXACT + ): + # draw left goal area + # top + img = cv2.line( + img, + ( + goal_area_left_start[0], + goal_area_left_start[1] - field_feature_size // 2, + ), + ( + goal_area_left_start[0], + goal_area_left_start[1] + field_feature_size // 2, + ), + color, + stroke_width, + ) + img = cv2.line( + img, + goal_area_left_start, + (goal_area_left_start[0] + field_feature_size, goal_area_left_start[1]), + color, + stroke_width, + ) + + # bottom + img = cv2.line( + img, + (goal_area_left_start[0], goal_area_left_end[1] - field_feature_size // 2), + (goal_area_left_start[0], goal_area_left_end[1] + field_feature_size // 2), + color, + stroke_width, + ) + img = cv2.line( + img, + (goal_area_left_start[0], goal_area_left_end[1]), + (goal_area_left_start[0] + field_feature_size, goal_area_left_end[1]), + color, + stroke_width, + ) + # draw right goal area + + # top + img = cv2.line( + img, + ( + goal_area_right_start[0], + goal_area_right_start[1] - field_feature_size // 2, + ), + ( + goal_area_right_start[0], + goal_area_right_start[1] + field_feature_size // 2, + ), + color, + stroke_width, + ) + img = cv2.line( + img, + goal_area_right_start, + (goal_area_right_start[0] - field_feature_size, goal_area_right_start[1]), + color, + stroke_width, + ) + + # bottom + img = cv2.line( + img, + ( + goal_area_right_start[0], + goal_area_right_end[1] - field_feature_size // 2, + ), + ( + goal_area_right_start[0], + goal_area_right_end[1] + field_feature_size // 2, + ), + color, + stroke_width, + ) + img = cv2.line( + img, + (goal_area_right_start[0], goal_area_right_end[1]), + (goal_area_right_start[0] - field_feature_size, goal_area_right_end[1]), + color, + stroke_width, + ) + + # draw center line to side line t crossings + # top + img = cv2.line( + img, + (middle_line_start[0] - field_feature_size // 2, middle_line_start[1]), + (middle_line_start[0] + field_feature_size // 2, middle_line_start[1]), + color, + stroke_width, + ) + img = cv2.line( + img, + middle_line_start, + (middle_line_start[0], middle_line_start[1] + field_feature_size), + color, + stroke_width, + ) + + # bottom + img = cv2.line( + img, + (middle_line_end[0] - field_feature_size // 2, middle_line_end[1]), + (middle_line_end[0] + field_feature_size // 2, middle_line_end[1]), + color, + stroke_width, + ) + img = cv2.line( + img, + middle_line_end, + (middle_line_end[0], middle_line_end[1] - field_feature_size), + color, + stroke_width, + ) + + if ( + target in [MapTypes.TCROSSINGS, MapTypes.FIELD_FEATURES] + and field_feature_style == FieldFeatureStyles.BLOB + ): + # draw blobs for goal areas + img = cv2.circle(img, goal_area_left_start, field_feature_size, color, -1) + img = cv2.circle( + img, + (goal_area_left_start[0], goal_area_left_end[1]), + field_feature_size, + color, + -1, + ) + img = cv2.circle(img, goal_area_right_start, field_feature_size, color, -1) + img = cv2.circle( + img, + (goal_area_right_start[0], goal_area_right_end[1]), + field_feature_size, + color, + -1, + ) + + # middle line + img = cv2.circle(img, middle_line_start, field_feature_size, color, -1) + img = cv2.circle(img, middle_line_end, field_feature_size, color, -1) + + if ( + target in [MapTypes.CROSSES, MapTypes.FIELD_FEATURES] + and field_feature_style == FieldFeatureStyles.EXACT + ): + # penalty marks + if penalty_mark: + drawCross( + img, penalty_mark_left, color, stroke_width, field_feature_size // 2 + ) + drawCross( + img, penalty_mark_right, color, stroke_width, field_feature_size // 2 + ) + + # middle point + if center_point: + drawCross(img, middle_point, color, stroke_width, field_feature_size // 2) + + # center circle middle line crossings + drawCross( + img, + (middle_point[0], middle_point[1] - center_circle_diameter), + color, + stroke_width, + field_feature_size // 2, + ) + drawCross( + img, + (middle_point[0], middle_point[1] + center_circle_diameter), + color, + stroke_width, + field_feature_size // 2, + ) + + if ( + target in [MapTypes.CROSSES, MapTypes.FIELD_FEATURES] + and field_feature_style == FieldFeatureStyles.BLOB + ): + # penalty marks + if penalty_mark: + img = cv2.circle(img, penalty_mark_left, field_feature_size, color, -1) + img = cv2.circle(img, penalty_mark_right, field_feature_size, color, -1) + + # middle point + if center_point: + img = cv2.circle(img, middle_point, field_feature_size, color, -1) + + # center circle middle line crossings + img = cv2.circle( + img, + (middle_point[0], middle_point[1] - center_circle_diameter), + field_feature_size, + color, + -1, + ) + img = cv2.circle( + img, + (middle_point[0], middle_point[1] + center_circle_diameter), + field_feature_size, + color, + -1, + ) + + # Create distance map + if distance_map: + img = drawDistance(img, distance_decay) + + # Invert + if invert: + img = 255 - img + + return img + + +def generate_metadata(parameters: dict, image_name: str) -> dict: + # Get the field dimensions in cm + field_dimensions = np.array( + [parameters["field_length"], parameters["field_width"], 0] + ) + # Add the border strip + field_dimensions[:2] += 2 * parameters["border_strip_width"] + # Get the origin + origin = -field_dimensions / 2 + # Convert to meters + origin /= 100 + + # Generate the metadata + return { + "image": image_name, + "resolution": 0.01, + "origin": origin.tolist(), + "occupied_thresh": 0.99, + "free_thresh": 0.196, + "negate": int(parameters["invert"]), + } + + +def load_config_file(file) -> Optional[dict]: + # Load the parameters from the file + config_file = yaml.load(file, Loader=yaml.FullLoader) + # Check if the file is valid (has the correct fields) + if ( + "header" in config_file + and "type" in config_file["header"] + and "version" in config_file["header"] + and "parameters" in config_file + and config_file["header"]["version"] == "1.0" + and config_file["header"]["type"] == "map_generator_config" + ): + return config_file["parameters"] diff --git a/soccer_field_map_generator/soccer_field_map_generator/gui.py b/soccer_field_map_generator/soccer_field_map_generator/gui.py new file mode 100644 index 0000000..dd20e41 --- /dev/null +++ b/soccer_field_map_generator/soccer_field_map_generator/gui.py @@ -0,0 +1,407 @@ +import cv2 +import os +import tkinter as tk +import yaml +from enum import Enum +from PIL import Image, ImageTk +from tkinter import filedialog +from tkinter import ttk + +from soccer_field_map_generator.generator import ( + MapTypes, + MarkTypes, + FieldFeatureStyles, + generate_map_image, + generate_metadata, + load_config_file, +) +from soccer_field_map_generator.tooltip import Tooltip + + +class MapGeneratorParamInput(tk.Frame): + def __init__( + self, parent, update_hook: callable, parameter_definitions: dict[str, dict] + ): + tk.Frame.__init__(self, parent) + + # Keep track of parameter definitions, GUI elements and the input values + self.parameter_definitions = parameter_definitions + self.parameter_ui_elements: dict[str, dict[str, ttk.Widget]] = {} + self.parameter_values: dict[str, tk.Variable] = {} + + # Generate GUI elements for all parameters + for parameter_name, parameter_definition in parameter_definitions.items(): + # Create GUI elements + label = ttk.Label(self, text=parameter_definition["label"]) + if parameter_definition["type"] == bool: + variable = tk.BooleanVar(value=parameter_definition["default"]) + ui_element = ttk.Checkbutton( + self, command=update_hook, variable=variable + ) + elif parameter_definition["type"] == int: + variable = tk.IntVar(value=parameter_definition["default"]) + ui_element = ttk.Entry(self, textvariable=variable) + ui_element.bind("", update_hook) + elif parameter_definition["type"] == float: + variable = tk.DoubleVar(value=parameter_definition["default"]) + ui_element = ttk.Entry(self, textvariable=variable) + ui_element.bind("", update_hook) + elif issubclass(parameter_definition["type"], Enum): + variable = tk.StringVar(value=parameter_definition["default"].value) + values = [enum.value for enum in parameter_definition["type"]] + ui_element = ttk.Combobox(self, values=values, textvariable=variable) + ui_element.bind("<>", update_hook) + else: + raise NotImplementedError("Parameter type not implemented") + + # Add tooltips + Tooltip(label, text=parameter_definition["tooltip"]) + Tooltip(ui_element, text=parameter_definition["tooltip"]) + + # Add ui elements to the dict + self.parameter_ui_elements[parameter_name] = { + "label": label, + "ui_element": ui_element, + } + + # Store variable for later state access + self.parameter_values[parameter_name] = variable + + # Create layout + for i, parameter_name in enumerate(parameter_definitions.keys()): + self.parameter_ui_elements[parameter_name]["label"].grid( + row=i, column=0, sticky="e" + ) + self.parameter_ui_elements[parameter_name]["ui_element"].grid( + row=i, column=1, sticky="w" + ) + + def get_parameters(self): + return { + parameter_name: parameter_value.get() + for parameter_name, parameter_value in self.parameter_values.items() + } + + def get_parameter(self, parameter_name): + return self.parameter_values[parameter_name].get() + + +class MapGeneratorGUI: + def __init__(self, root: tk.Tk): + # Set ttk theme + s = ttk.Style() + s.theme_use("clam") + + # Set window title and size + self.root = root + self.root.title("Map Generator GUI") + self.root.resizable(False, False) + + # Create GUI elements + + # Title + self.title = ttk.Label( + self.root, text="Soccer Map Generator", font=("TkDefaultFont", 16) + ) + + # Parameter Input + self.parameter_input = MapGeneratorParamInput( + self.root, + self.update_map, + { + "map_type": { + "type": MapTypes, + "default": MapTypes.LINE, + "label": "Map Type", + "tooltip": "Type of the map we want to generate", + }, + "penalty_mark": { + "type": bool, + "default": True, + "label": "Penalty Mark", + "tooltip": "Whether or not to draw the penalty mark", + }, + "center_point": { + "type": bool, + "default": True, + "label": "Center Point", + "tooltip": "Whether or not to draw the center point", + }, + "goal_back": { + "type": bool, + "default": True, + "label": "Goal Back", + "tooltip": "Whether or not to draw the back area of the goal", + }, + "stroke_width": { + "type": int, + "default": 5, + "label": "Stoke Width", + "tooltip": "Width (in px) of the shapes we draw", + }, + "field_length": { + "type": int, + "default": 900, + "label": "Field Length", + "tooltip": "Length of the field in cm", + }, + "field_width": { + "type": int, + "default": 600, + "label": "Field Width", + "tooltip": "Width of the field in cm", + }, + "goal_depth": { + "type": int, + "default": 60, + "label": "Goal Depth", + "tooltip": "Depth of the goal in cm", + }, + "goal_width": { + "type": int, + "default": 260, + "label": "Goal Width", + "tooltip": "Width of the goal in cm", + }, + "goal_area_length": { + "type": int, + "default": 100, + "label": "Goal Area Length", + "tooltip": "Length of the goal area in cm", + }, + "goal_area_width": { + "type": int, + "default": 300, + "label": "Goal Area Width", + "tooltip": "Width of the goal area in cm", + }, + "penalty_mark_distance": { + "type": int, + "default": 150, + "label": "Penalty Mark Distance", + "tooltip": "Distance of the penalty mark from the goal line in cm", + }, + "center_circle_diameter": { + "type": int, + "default": 150, + "label": "Center Circle Diameter", + "tooltip": "Diameter of the center circle in cm", + }, + "border_strip_width": { + "type": int, + "default": 100, + "label": "Border Strip Width", + "tooltip": "Width of the border strip around the field in cm", + }, + "penalty_area_length": { + "type": int, + "default": 200, + "label": "Penalty Area Length", + "tooltip": "Length of the penalty area in cm", + }, + "penalty_area_width": { + "type": int, + "default": 500, + "label": "Penalty Area Width", + "tooltip": "Width of the penalty area in cm", + }, + "field_feature_size": { + "type": int, + "default": 30, + "label": "Field Feature Size", + "tooltip": "Size of the field features in cm", + }, + "mark_type": { + "type": MarkTypes, + "default": MarkTypes.CROSS, + "label": "Mark Type", + "tooltip": "Type of the marks (penalty mark, center point)", + }, + "field_feature_style": { + "type": FieldFeatureStyles, + "default": FieldFeatureStyles.EXACT, + "label": "Field Feature Style", + "tooltip": "Style of the field features", + }, + "distance_map": { + "type": bool, + "default": False, + "label": "Distance Map", + "tooltip": "Whether or not to draw the distance map", + }, + "distance_decay": { + "type": float, + "default": 0.0, + "label": "Distance Decay", + "tooltip": "Exponential decay applied to the distance map", + }, + "invert": { + "type": bool, + "default": True, + "label": "Invert", + "tooltip": "Invert the final image", + }, + }, + ) + + # Generate Map Button + self.save_map_button = ttk.Button( + self.root, text="Save Map", command=self.save_map + ) + + # Save metadata checkbox + self.save_metadata = tk.BooleanVar(value=True) + self.save_metadata_checkbox = ttk.Checkbutton( + self.root, text="Save Metadata", variable=self.save_metadata + ) + + # Load and save config buttons + self.load_config_button = ttk.Button( + self.root, text="Load Config", command=self.load_config + ) + self.save_config_button = ttk.Button( + self.root, text="Save Config", command=self.save_config + ) + + # Canvas to display the generated map + self.canvas = tk.Canvas(self.root, width=800, height=600) + + # Layout + + # Parameter input and generate button + self.title.grid(row=0, column=0, columnspan=2, pady=20, padx=10) + self.parameter_input.grid(row=1, column=0, columnspan=2, pady=10, padx=10) + self.load_config_button.grid(row=2, column=0, columnspan=1, pady=10) + self.save_config_button.grid(row=2, column=1, columnspan=1, pady=10) + self.save_metadata_checkbox.grid(row=3, column=0, columnspan=1, pady=10) + self.save_map_button.grid(row=3, column=1, columnspan=1, pady=10) + + # Preview + self.canvas.grid(row=0, column=2, rowspan=4, pady=10, padx=30) + + # Color in which we want to draw the lines + self.primary_color = (255, 255, 255) # white + + # Render initial map + self.root.update() # We need to call update() first + self.update_map() + + def load_config(self): + # Prompt the user to select a file (force yaml) + file = filedialog.askopenfile( + mode="r", + defaultextension=".yaml", + filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")), + ) + if file: + # Load the config file + config = load_config_file(file) + if config is None: + # Show error box and return if the file is invalid + tk.messagebox.showerror("Error", "Invalid config file") + return + # Set the parameters in the gui + for parameter_name, parameter_value in config.items(): + self.parameter_input.parameter_values[parameter_name].set( + parameter_value + ) + # Update the map + self.update_map() + + def save_config(self): + # Get the parameters from the user input + parameters = self.parameter_input.get_parameters() + # Open a file dialog to select the file + file = filedialog.asksaveasfile( + mode="w", + defaultextension=".yaml", + filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")), + ) + if file: + # Add header + file.write("# Map Generator Config\n") + file.write("# This file was generated by the map generator GUI\n\n") + # Save the parameters in this format: + yaml.dump( + { + "header": {"version": "1.0", "type": "map_generator_config"}, + "parameters": parameters, + }, + file, + sort_keys=False, + ) + print(f"Saved config to {file.name}") + + def save_map(self): + file = filedialog.asksaveasfile( + mode="w", + defaultextension=".png", + filetypes=(("png file", "*.png"), ("All Files", "*.*")), + ) + if file: + print(f"Saving map to {file.name}") + + # Generate and save the map + parameters = self.parameter_input.get_parameters() + generated_map = generate_map_image(parameters) + if cv2.imwrite(file.name, generated_map): + # Save metadata + if self.save_metadata.get(): + # Save the metadata in this format: + metadata = generate_metadata( + parameters, os.path.basename(file.name) + ) + # Save metadata in the same folder as the map + metadata_file = os.path.join( + os.path.dirname(file.name), "map_server.yaml" + ) + with open(metadata_file, "w") as f: + yaml.dump(metadata, f, sort_keys=False) + print(f"Saved metadata to {metadata_file}") + + # Show success box and ask if we want to open it with the default image viewer + if tk.messagebox.askyesno( + "Success", "Map saved successfully. Do you want to open it?" + ): + import platform + import subprocess + + if platform.system() == "Windows": + subprocess.Popen(["start", file.name], shell=True) + elif platform.system() == "Darwin": + subprocess.Popen(["open", file.name]) + else: + subprocess.Popen(["xdg-open", file.name]) + else: + # Show error box + tk.messagebox.showerror("Error", "Could not save map to file") + + def update_map(self, *args): + # Generate and display the map on the canvas + try: + generated_map = generate_map_image(self.parameter_input.get_parameters()) + self.display_map(generated_map) + except tk.TclError as e: + print(f"Invalid input for map generation. '{e}'") + + def display_map(self, image): + # Display the generated map on the canvas + img = Image.fromarray(image) + # Resize to fit canvas while keeping aspect ratio + img.thumbnail( + (self.canvas.winfo_width(), self.canvas.winfo_height()), + Image.Resampling.LANCZOS, + ) + img = ImageTk.PhotoImage(img) + self.canvas.create_image(0, 0, anchor=tk.NW, image=img) + self.canvas.image = img # To prevent garbage collection + + +def main(): + root = tk.Tk() + app = MapGeneratorGUI(root) + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/soccer_field_map_generator/soccer_field_map_generator/tooltip.py b/soccer_field_map_generator/soccer_field_map_generator/tooltip.py new file mode 100644 index 0000000..b92970f --- /dev/null +++ b/soccer_field_map_generator/soccer_field_map_generator/tooltip.py @@ -0,0 +1,151 @@ +import tkinter as tk + + +class Tooltip: + """ + It creates a tooltip for a given widget as the mouse goes on it. + + see: + + http://stackoverflow.com/questions/3221956/ + what-is-the-simplest-way-to-make-tooltips- + in-tkinter/36221216#36221216 + + http://www.daniweb.com/programming/software-development/ + code/484591/a-tooltip-class-for-tkinter + + - Originally written by vegaseat on 2014.09.09. + + - Modified to include a delay time by Victor Zaccardo on 2016.03.25. + + - Modified + - to correct extreme right and extreme bottom behavior, + - to stay inside the screen whenever the tooltip might go out on + the top but still the screen is higher than the tooltip, + - to use the more flexible mouse positioning, + - to add customizable background color, padding, waittime and + wraplength on creation + by Alberto Vassena on 2016.11.05. + + Tested on Ubuntu 16.04/16.10, running Python 3.5.2 + + TODO: themes styles support + """ + + def __init__( + self, + widget, + *, + bg="#FFFFEA", + pad=(5, 3, 5, 3), + text="widget info", + waittime=400, + wraplength=250 + ): + self.waittime = waittime # in miliseconds, originally 500 + self.wraplength = wraplength # in pixels, originally 180 + self.widget = widget + self.text = text + self.widget.bind("", self.onEnter) + self.widget.bind("", self.onLeave) + self.widget.bind("", self.onLeave) + self.bg = bg + self.pad = pad + self.id = None + self.tw = None + + def onEnter(self, event=None): + self.schedule() + + def onLeave(self, event=None): + self.unschedule() + self.hide() + + def schedule(self): + self.unschedule() + self.id = self.widget.after(self.waittime, self.show) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.widget.after_cancel(id_) + + def show(self): + def tip_pos_calculator(widget, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): + w = widget + + s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight() + + width, height = ( + pad[0] + label.winfo_reqwidth() + pad[2], + pad[1] + label.winfo_reqheight() + pad[3], + ) + + mouse_x, mouse_y = w.winfo_pointerxy() + + x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] + x2, y2 = x1 + width, y1 + height + + x_delta = x2 - s_width + if x_delta < 0: + x_delta = 0 + y_delta = y2 - s_height + if y_delta < 0: + y_delta = 0 + + offscreen = (x_delta, y_delta) != (0, 0) + + if offscreen: + if x_delta: + x1 = mouse_x - tip_delta[0] - width + + if y_delta: + y1 = mouse_y - tip_delta[1] - height + + offscreen_again = y1 < 0 # out on the top + + if offscreen_again: + # No further checks will be done. + + # TIP: + # A further mod might automagically augment the + # wraplength when the tooltip is too high to be + # kept inside the screen. + y1 = 0 + + return x1, y1 + + bg = self.bg + pad = self.pad + widget = self.widget + + # creates a toplevel window + self.tw = tk.Toplevel(widget) + + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + + win = tk.Frame(self.tw, background=bg, borderwidth=0) + label = tk.Label( + win, + text=self.text, + justify=tk.LEFT, + background=bg, + relief=tk.SOLID, + borderwidth=0, + wraplength=self.wraplength, + ) + + label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) + win.grid() + + x, y = tip_pos_calculator(widget, label) + + self.tw.wm_geometry("+%d+%d" % (x, y)) + + def hide(self): + tw = self.tw + if tw: + tw.destroy() + self.tw = None diff --git a/soccer_field_map_generator/test/test_copyright.py b/soccer_field_map_generator/test/test_copyright.py new file mode 100644 index 0000000..97a3919 --- /dev/null +++ b/soccer_field_map_generator/test/test_copyright.py @@ -0,0 +1,25 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_copyright.main import main +import pytest + + +# Remove the `skip` decorator once the source file(s) have a copyright header +@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/soccer_field_map_generator/test/test_flake8.py b/soccer_field_map_generator/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/soccer_field_map_generator/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/soccer_field_map_generator/test/test_pep257.py b/soccer_field_map_generator/test/test_pep257.py new file mode 100644 index 0000000..b234a38 --- /dev/null +++ b/soccer_field_map_generator/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' From c78ecbf3b508f515187285dabad328cdd02fd98f Mon Sep 17 00:00:00 2001 From: Florian Vahl <7vahl@informatik.uni-hamburg.de> Date: Thu, 21 Dec 2023 14:44:38 +0100 Subject: [PATCH 2/8] Add basic readme --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- gui.png | Bin 0 -> 77720 bytes 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 gui.png diff --git a/README.md b/README.md index c64ee00..3b79b3c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,45 @@ -# Template Repo +# Soccer Field Map Generator [![Build and Test (humble)](../../actions/workflows/build_and_test_humble.yaml/badge.svg?branch=rolling)](../../actions/workflows/build_and_test_humble.yaml?query=branch:rolling) [![Build and Test (iron)](../../actions/workflows/build_and_test_iron.yaml/badge.svg?branch=rolling)](../../actions/workflows/build_and_test_iron.yaml?query=branch:rolling) [![Build and Test (rolling)](../../actions/workflows/build_and_test_rolling.yaml/badge.svg?branch=rolling)](../../actions/workflows/build_and_test_rolling.yaml?query=branch:rolling) + +This repository contains a tool for generating soccer field maps. It includes a GUI for interactively creating and editing maps, as well as a command-line interface for scripted map generation. + +## Installation + +To install the tool, run the following commands in your colcon workspace: + +```bash +git clone git@github.com:ros-sports/soccer_field_map_generator.git src/soccer_field_map_generator +rosdep install --from-paths src --ignore-src -r -y +colcon build +``` + +Don't forget to source your workspace after building: + +```bash +source install/setup.bash +``` + +## Usage + +### GUI + +To launch the GUI, run the following command: + +```bash +ros2 run soccer_field_map_generator gui +``` + +You should see a window like this: + +![GUI](gui.png) + +### CLI + +To generate a map using the command-line interface, run the following command: + +```bash +ros2 run soccer_field_map_generator cli [output_file] [config_file] [options] +``` diff --git a/gui.png b/gui.png new file mode 100644 index 0000000000000000000000000000000000000000..634370d323493a5cbeff93e05c84fdec3a04c837 GIT binary patch literal 77720 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 Date: Fri, 22 Dec 2023 00:08:29 +0100 Subject: [PATCH 3/8] Cleanup --- .gitignore | 5 +++++ .../__pycache__/__init__.cpython-310.pyc | Bin 188 -> 0 bytes .../__pycache__/generator.cpython-310.pyc | Bin 8198 -> 0 bytes .../__pycache__/gui.cpython-310.pyc | Bin 9345 -> 0 bytes .../__pycache__/tooltip.cpython-310.pyc | Bin 3960 -> 0 bytes 5 files changed, 5 insertions(+) create mode 100644 .gitignore delete mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/__init__.cpython-310.pyc delete mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/generator.cpython-310.pyc delete mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/gui.cpython-310.pyc delete mode 100644 soccer_field_map_generator/soccer_field_map_generator/__pycache__/tooltip.cpython-310.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59acd29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +.vscode/* +log/* + +*.pyc \ No newline at end of file diff --git a/soccer_field_map_generator/soccer_field_map_generator/__pycache__/__init__.cpython-310.pyc b/soccer_field_map_generator/soccer_field_map_generator/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 0e3e9c8e732c9ffba78924850fd83d05115564d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 zcmd1j<>g`k0;8WzsUZ3>h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o10gKO;XkwOHTK zyeu&zM?X0~CpkYazPwmJsWdYuMZY*dIXSf`J}omfCnY{Nu^>J@H7~U&u_V6;pHzH& fW?p7Ve7s&k^`TZ-~`szm=RR9?le_eT(>Ftw>v0 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 diff --git a/soccer_field_map_generator/soccer_field_map_generator/__pycache__/tooltip.cpython-310.pyc b/soccer_field_map_generator/soccer_field_map_generator/__pycache__/tooltip.cpython-310.pyc deleted file mode 100644 index 3396f80a12e88bd0464f01c4f328df77035969f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3960 zcmaJ^&2QVt73Y^o>ch6;>^jc21=vjstlCJjosTwY5F~AqBHPqiAnq10iV_rOB+;fw zWk^|;L0!7J1}M<$9_(ZOkKTLdwI}}vyL)K+dqYWT~Yo?gXKrZ;0apt2?$e|8Y=thZxyC7{hh+}ue5!Qsc)2qagOiGu1|FNF)?_8 zmh6LYtYr$V*w;w{Ggyh4%=)^-Y*v1!?HlX{tFqcVb-#q(3R^|bWNWPcPT9BEHMWko z&91YL@Gi5D*#_PfR>A(8xwRL^VHym*e*kmE!Sgb8e8IhxCywW&#aE{r3ygZf5s#d4 zziLHq59 zCutBzN&C*u&h~?Q_uFzMVZbx)ducOBn%JY61cPCS$<47hX{K>=;2m0uD1T(s}pht!?z`?uzuOK`^Haf^9TFyvG@CJh*>a~-&n~pufn~WdvFdhzg zlwQK-$Beh{{_^g<2it9L4y5UaUXnDSsX2sV!>03E1ic{g!f@h@MUbY@VQ1nT@t&7J z&!o1Ut?j$5tq1fsU-bK!1>Jx%n7|1lKO8X*3ossf6DJJ@oJ4#X_$dst@A$3Z8Yd*(1s0`f8#_R1=cF~=VzIAd_)(Si;Jn3n=?bcm|KWg@LxWTU&DjInEwj8-|92 zKXgV>I&!vQ!nq!XQG!lj*iv#-_=F&r=EHsk)8?=OhWTi}TmCOWHGZj?J3{X8&4zyE)fpbtg(^O?P*6=o$ z#x$=Ohh0&lFN@Do%7yR`inxyN z-~GAWk717X_AidSe%OW|`ElfqlXhnmgbb<0_qlKp*dcQV-q7tKT!e=x{(qllanu@4 za@%!-C`euR7IsTEK+KA%(yuOmMZcn+)c1<~*qSfcU04i@6tW&QX)!ukrkpBgii&FW zM){!bHq_is{61%+kbls`I%ec01a_3la~9+yaK$Rf1GI$1)zp)<`NlKV{_kyR`FvZv z)%o^WREGW3NcB@A zGtMX-AUzzEPEBUgDN_BwqH(4%i`nn>X$c&gQpiCWy-KEmRvFEyvGRLsYG&raN~SaG zrUF{IsdP2Gt8!GM5$3P5mG?RlOzCum{0d3P$jq;_KWS5IYRlc%*lK2Fc30a_=DKXG z?KZS?nK8&m1Q`O3SjGj(Md?jFnHd%yO}3(_r@3#?l5apVWr|Z$FoD9SHEmh~Vx6hL z2g{u)Hwd+Mvv}*0tIxOwaj}9h8mGpY0=(sEB`ZTWSdW#^2E!UTt2(V^)yx97mf4cC zGF{D9veKq9UCY*(dZ>ucnTq~uW`bAG>XNqyUOijO>RpYM4)y7^?3x&6Yoa^fXH&^6 z%&%s3@Mk-MXM}$k!HK*Xwyf zbS1{}b@CrvL=cXlKME<`{|*3?Ef7Op*EaMG)%;hj^oh2v+M10JwDB|4nr3J+9JM<5 z=v%gS^4UBvDf6@*g)xLCk6Yj0Voxfb^616ICa-p4fkY~g-!O|(XdU1d6m%!zA@Jm; z;92>sK$I6(!v0VNGuA zzIpz~yb38jj%u5VNuyjOUn)db0qF1@(V%_R<6NCwF^>2VzI=|BdbiNwbq6t3a`bDi`*!4o zvoDtGve*}rGvDaMld-{j`@qXO__N*hJ#RMz$X05gEiY5)KL From 2ba4f6e5cc7563f58b39645748fac14a5087b0fb Mon Sep 17 00:00:00 2001 From: Florian Vahl Date: Fri, 22 Dec 2023 00:14:56 +0100 Subject: [PATCH 4/8] Fix some linting errors --- .../soccer_field_map_generator/generator.py | 11 ++++------- .../soccer_field_map_generator/gui.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/soccer_field_map_generator/soccer_field_map_generator/generator.py b/soccer_field_map_generator/soccer_field_map_generator/generator.py index c3476d2..aba38a8 100755 --- a/soccer_field_map_generator/soccer_field_map_generator/generator.py +++ b/soccer_field_map_generator/soccer_field_map_generator/generator.py @@ -169,11 +169,8 @@ def generate_map_image(parameters): goalpost_right_1 = (image_size[1] - goalpost_left_1[0], goalpost_left_1[1]) goalpost_right_2 = (image_size[1] - goalpost_left_2[0], goalpost_left_2[1]) - goal_back_corner_left_1 = (goalpost_left_1[0] - goal_depth, goalpost_left_1[1]) - goal_back_corner_left_2 = (goalpost_left_2[0] - goal_depth, goalpost_left_2[1]) - - goal_back_corner_right_1 = (goalpost_right_1[0] + goal_depth, goalpost_right_1[1]) - goal_back_corner_right_2 = (goalpost_right_2[0] + goal_depth, goalpost_right_2[1]) + goal_back_corner_left = (goalpost_left_2[0] - goal_depth, goalpost_left_2[1]) + goal_back_corner_right = (goalpost_right_2[0] + goal_depth, goalpost_right_2[1]) # Create black image in the correct size img = np.zeros(image_size, np.uint8) @@ -232,10 +229,10 @@ def generate_map_image(parameters): # Draw goal back area if goal_back: img = cv2.rectangle( - img, goalpost_left_1, goal_back_corner_left_2, color, stroke_width + img, goalpost_left_1, goal_back_corner_left, color, stroke_width ) img = cv2.rectangle( - img, goalpost_right_1, goal_back_corner_right_2, color, stroke_width + img, goalpost_right_1, goal_back_corner_right, color, stroke_width ) if target == MapTypes.POSTS: diff --git a/soccer_field_map_generator/soccer_field_map_generator/gui.py b/soccer_field_map_generator/soccer_field_map_generator/gui.py index dd20e41..53fc0ac 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/gui.py +++ b/soccer_field_map_generator/soccer_field_map_generator/gui.py @@ -399,7 +399,7 @@ def display_map(self, image): def main(): root = tk.Tk() - app = MapGeneratorGUI(root) + MapGeneratorGUI(root) root.mainloop() From 3dd8313844179454a2697c03c0181f6817530d5d Mon Sep 17 00:00:00 2001 From: Florian Vahl Date: Fri, 22 Dec 2023 00:44:07 +0100 Subject: [PATCH 5/8] Make ci happy --- .../soccer_field_map_generator/cli.py | 49 ++- .../soccer_field_map_generator/generator.py | 125 +++--- .../soccer_field_map_generator/gui.py | 359 +++++++++--------- .../soccer_field_map_generator/tooltip.py | 12 +- 4 files changed, 294 insertions(+), 251 deletions(-) diff --git a/soccer_field_map_generator/soccer_field_map_generator/cli.py b/soccer_field_map_generator/soccer_field_map_generator/cli.py index d3f5166..161a0be 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/cli.py +++ b/soccer_field_map_generator/soccer_field_map_generator/cli.py @@ -1,42 +1,57 @@ -import sys -import os -import cv2 -import yaml +# Copyright (c) 2023 Hamburg Bit-Bots +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + import argparse +import os +import sys +import cv2 from soccer_field_map_generator.generator import ( generate_map_image, generate_metadata, load_config_file, ) +import yaml def main(): - parser = argparse.ArgumentParser(description="Generate maps for localization") - parser.add_argument("output", help="Output file name") + parser = argparse.ArgumentParser(description='Generate maps for localization') + parser.add_argument('output', help='Output file name') parser.add_argument( - "config", - help="Config file for the generator that specifies the parameters for the map generation", + 'config', + help='Config file for the generator that specifies the parameters for the map generation', ) parser.add_argument( - "--metadata", + '--metadata', help="Also generates a 'map_server.yaml' file with the metadata for the map", - action="store_true", + action='store_true', ) args = parser.parse_args() # Check if the config file exists if not os.path.isfile(args.config): - print("Config file does not exist") + print('Config file does not exist') sys.exit(1) # Load config file - with open(args.config, "r") as config_file: + with open(args.config, 'r') as config_file: parameters = load_config_file(config_file) # Check if the config file is valid if parameters is None: - print("Invalid config file") + print('Invalid config file') sys.exit(1) # Generate the map image @@ -47,7 +62,7 @@ def main(): # Check if the output folder exists if not os.path.isdir(os.path.dirname(output_path)): - print("Output folder does not exist") + print('Output folder does not exist') sys.exit(1) # Save the image @@ -57,11 +72,11 @@ def main(): if args.metadata: metadata = generate_metadata(parameters, os.path.basename(output_path)) metadata_file_name = os.path.join( - os.path.dirname(output_path), "map_server.yaml" + os.path.dirname(output_path), 'map_server.yaml' ) - with open(metadata_file_name, "w") as metadata_file: + with open(metadata_file_name, 'w') as metadata_file: yaml.dump(metadata, metadata_file, sort_keys=False) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/soccer_field_map_generator/soccer_field_map_generator/generator.py b/soccer_field_map_generator/soccer_field_map_generator/generator.py index aba38a8..2bcb768 100755 --- a/soccer_field_map_generator/soccer_field_map_generator/generator.py +++ b/soccer_field_map_generator/soccer_field_map_generator/generator.py @@ -1,33 +1,46 @@ -#!/usr/bin/env python3 +# Copyright (c) 2023 Hamburg Bit-Bots +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum import math -import yaml +from typing import Optional import cv2 import numpy as np -from typing import Optional from scipy import ndimage -from enum import Enum +import yaml class MapTypes(Enum): - LINE = "line" - POSTS = "posts" - FIELD_BOUNDARY = "field_boundary" - FIELD_FEATURES = "field_features" - CORNERS = "corners" - TCROSSINGS = "tcrossings" - CROSSES = "crosses" + LINE = 'line' + POSTS = 'posts' + FIELD_BOUNDARY = 'field_boundary' + FIELD_FEATURES = 'field_features' + CORNERS = 'corners' + TCROSSINGS = 'tcrossings' + CROSSES = 'crosses' class MarkTypes(Enum): - POINT = "point" - CROSS = "cross" + POINT = 'point' + CROSS = 'cross' class FieldFeatureStyles(Enum): - EXACT = "exact" - BLOB = "blob" + EXACT = 'exact' + BLOB = 'blob' def drawCross(img, point, color, width=5, length=15): @@ -62,30 +75,30 @@ def drawDistance(image, decay_factor): def generate_map_image(parameters): - target = MapTypes(parameters["map_type"]) - mark_type = MarkTypes(parameters["mark_type"]) - field_feature_style = FieldFeatureStyles(parameters["field_feature_style"]) - - penalty_mark = parameters["penalty_mark"] - center_point = parameters["center_point"] - goal_back = parameters["goal_back"] # Draw goal back area - - stroke_width = parameters["stroke_width"] - field_length = parameters["field_length"] - field_width = parameters["field_width"] - goal_depth = parameters["goal_depth"] - goal_width = parameters["goal_width"] - goal_area_length = parameters["goal_area_length"] - goal_area_width = parameters["goal_area_width"] - penalty_mark_distance = parameters["penalty_mark_distance"] - center_circle_diameter = parameters["center_circle_diameter"] - border_strip_width = parameters["border_strip_width"] - penalty_area_length = parameters["penalty_area_length"] - penalty_area_width = parameters["penalty_area_width"] - field_feature_size = parameters["field_feature_size"] - distance_map = parameters["distance_map"] - distance_decay = parameters["distance_decay"] - invert = parameters["invert"] + target = MapTypes(parameters['map_type']) + mark_type = MarkTypes(parameters['mark_type']) + field_feature_style = FieldFeatureStyles(parameters['field_feature_style']) + + penalty_mark = parameters['penalty_mark'] + center_point = parameters['center_point'] + goal_back = parameters['goal_back'] # Draw goal back area + + stroke_width = parameters['stroke_width'] + field_length = parameters['field_length'] + field_width = parameters['field_width'] + goal_depth = parameters['goal_depth'] + goal_width = parameters['goal_width'] + goal_area_length = parameters['goal_area_length'] + goal_area_width = parameters['goal_area_width'] + penalty_mark_distance = parameters['penalty_mark_distance'] + center_circle_diameter = parameters['center_circle_diameter'] + border_strip_width = parameters['border_strip_width'] + penalty_area_length = parameters['penalty_area_length'] + penalty_area_width = parameters['penalty_area_width'] + field_feature_size = parameters['field_feature_size'] + distance_map = parameters['distance_map'] + distance_decay = parameters['distance_decay'] + invert = parameters['invert'] # Color of the lines and marks color = (255, 255, 255) # white @@ -197,7 +210,7 @@ def generate_map_image(parameters): elif mark_type == MarkTypes.CROSS: drawCross(img, middle_point, color, stroke_width) else: - raise NotImplementedError("Mark type not implemented") + raise NotImplementedError('Mark type not implemented') # Draw penalty marks (point or cross) if penalty_mark: @@ -208,7 +221,7 @@ def generate_map_image(parameters): drawCross(img, penalty_mark_left, color, stroke_width) drawCross(img, penalty_mark_right, color, stroke_width) else: - raise NotImplementedError("Mark type not implemented") + raise NotImplementedError('Mark type not implemented') # Draw goal area img = cv2.rectangle( @@ -657,10 +670,10 @@ def generate_map_image(parameters): def generate_metadata(parameters: dict, image_name: str) -> dict: # Get the field dimensions in cm field_dimensions = np.array( - [parameters["field_length"], parameters["field_width"], 0] + [parameters['field_length'], parameters['field_width'], 0] ) # Add the border strip - field_dimensions[:2] += 2 * parameters["border_strip_width"] + field_dimensions[:2] += 2 * parameters['border_strip_width'] # Get the origin origin = -field_dimensions / 2 # Convert to meters @@ -668,12 +681,12 @@ def generate_metadata(parameters: dict, image_name: str) -> dict: # Generate the metadata return { - "image": image_name, - "resolution": 0.01, - "origin": origin.tolist(), - "occupied_thresh": 0.99, - "free_thresh": 0.196, - "negate": int(parameters["invert"]), + 'image': image_name, + 'resolution': 0.01, + 'origin': origin.tolist(), + 'occupied_thresh': 0.99, + 'free_thresh': 0.196, + 'negate': int(parameters['invert']), } @@ -682,11 +695,11 @@ def load_config_file(file) -> Optional[dict]: config_file = yaml.load(file, Loader=yaml.FullLoader) # Check if the file is valid (has the correct fields) if ( - "header" in config_file - and "type" in config_file["header"] - and "version" in config_file["header"] - and "parameters" in config_file - and config_file["header"]["version"] == "1.0" - and config_file["header"]["type"] == "map_generator_config" + 'header' in config_file + and 'type' in config_file['header'] + and 'version' in config_file['header'] + and 'parameters' in config_file + and config_file['header']['version'] == '1.0' + and config_file['header']['type'] == 'map_generator_config' ): - return config_file["parameters"] + return config_file['parameters'] diff --git a/soccer_field_map_generator/soccer_field_map_generator/gui.py b/soccer_field_map_generator/soccer_field_map_generator/gui.py index 53fc0ac..b6eec78 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/gui.py +++ b/soccer_field_map_generator/soccer_field_map_generator/gui.py @@ -1,21 +1,36 @@ -import cv2 +# Copyright (c) 2023 Hamburg Bit-Bots +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from enum import Enum import os import tkinter as tk -import yaml -from enum import Enum -from PIL import Image, ImageTk from tkinter import filedialog from tkinter import ttk +import cv2 +from PIL import Image, ImageTk from soccer_field_map_generator.generator import ( - MapTypes, - MarkTypes, FieldFeatureStyles, generate_map_image, generate_metadata, load_config_file, + MapTypes, + MarkTypes, ) from soccer_field_map_generator.tooltip import Tooltip +import yaml class MapGeneratorParamInput(tk.Frame): @@ -32,36 +47,36 @@ def __init__( # Generate GUI elements for all parameters for parameter_name, parameter_definition in parameter_definitions.items(): # Create GUI elements - label = ttk.Label(self, text=parameter_definition["label"]) - if parameter_definition["type"] == bool: - variable = tk.BooleanVar(value=parameter_definition["default"]) + label = ttk.Label(self, text=parameter_definition['label']) + if parameter_definition['type'] == bool: + variable = tk.BooleanVar(value=parameter_definition['default']) ui_element = ttk.Checkbutton( self, command=update_hook, variable=variable ) - elif parameter_definition["type"] == int: - variable = tk.IntVar(value=parameter_definition["default"]) + elif parameter_definition['type'] == int: + variable = tk.IntVar(value=parameter_definition['default']) ui_element = ttk.Entry(self, textvariable=variable) - ui_element.bind("", update_hook) - elif parameter_definition["type"] == float: - variable = tk.DoubleVar(value=parameter_definition["default"]) + ui_element.bind('', update_hook) + elif parameter_definition['type'] == float: + variable = tk.DoubleVar(value=parameter_definition['default']) ui_element = ttk.Entry(self, textvariable=variable) - ui_element.bind("", update_hook) - elif issubclass(parameter_definition["type"], Enum): - variable = tk.StringVar(value=parameter_definition["default"].value) - values = [enum.value for enum in parameter_definition["type"]] + ui_element.bind('', update_hook) + elif issubclass(parameter_definition['type'], Enum): + variable = tk.StringVar(value=parameter_definition['default'].value) + values = [enum.value for enum in parameter_definition['type']] ui_element = ttk.Combobox(self, values=values, textvariable=variable) - ui_element.bind("<>", update_hook) + ui_element.bind('<>', update_hook) else: - raise NotImplementedError("Parameter type not implemented") + raise NotImplementedError('Parameter type not implemented') # Add tooltips - Tooltip(label, text=parameter_definition["tooltip"]) - Tooltip(ui_element, text=parameter_definition["tooltip"]) + Tooltip(label, text=parameter_definition['tooltip']) + Tooltip(ui_element, text=parameter_definition['tooltip']) # Add ui elements to the dict self.parameter_ui_elements[parameter_name] = { - "label": label, - "ui_element": ui_element, + 'label': label, + 'ui_element': ui_element, } # Store variable for later state access @@ -69,11 +84,11 @@ def __init__( # Create layout for i, parameter_name in enumerate(parameter_definitions.keys()): - self.parameter_ui_elements[parameter_name]["label"].grid( - row=i, column=0, sticky="e" + self.parameter_ui_elements[parameter_name]['label'].grid( + row=i, column=0, sticky='e' ) - self.parameter_ui_elements[parameter_name]["ui_element"].grid( - row=i, column=1, sticky="w" + self.parameter_ui_elements[parameter_name]['ui_element'].grid( + row=i, column=1, sticky='w' ) def get_parameters(self): @@ -90,18 +105,18 @@ class MapGeneratorGUI: def __init__(self, root: tk.Tk): # Set ttk theme s = ttk.Style() - s.theme_use("clam") + s.theme_use('clam') # Set window title and size self.root = root - self.root.title("Map Generator GUI") + self.root.title('Map Generator GUI') self.root.resizable(False, False) # Create GUI elements # Title self.title = ttk.Label( - self.root, text="Soccer Map Generator", font=("TkDefaultFont", 16) + self.root, text='Soccer Map Generator', font=('TkDefaultFont', 16) ) # Parameter Input @@ -109,158 +124,158 @@ def __init__(self, root: tk.Tk): self.root, self.update_map, { - "map_type": { - "type": MapTypes, - "default": MapTypes.LINE, - "label": "Map Type", - "tooltip": "Type of the map we want to generate", + 'map_type': { + 'type': MapTypes, + 'default': MapTypes.LINE, + 'label': 'Map Type', + 'tooltip': 'Type of the map we want to generate', }, - "penalty_mark": { - "type": bool, - "default": True, - "label": "Penalty Mark", - "tooltip": "Whether or not to draw the penalty mark", + 'penalty_mark': { + 'type': bool, + 'default': True, + 'label': 'Penalty Mark', + 'tooltip': 'Whether or not to draw the penalty mark', }, - "center_point": { - "type": bool, - "default": True, - "label": "Center Point", - "tooltip": "Whether or not to draw the center point", + 'center_point': { + 'type': bool, + 'default': True, + 'label': 'Center Point', + 'tooltip': 'Whether or not to draw the center point', }, - "goal_back": { - "type": bool, - "default": True, - "label": "Goal Back", - "tooltip": "Whether or not to draw the back area of the goal", + 'goal_back': { + 'type': bool, + 'default': True, + 'label': 'Goal Back', + 'tooltip': 'Whether or not to draw the back area of the goal', }, - "stroke_width": { - "type": int, - "default": 5, - "label": "Stoke Width", - "tooltip": "Width (in px) of the shapes we draw", + 'stroke_width': { + 'type': int, + 'default': 5, + 'label': 'Stoke Width', + 'tooltip': 'Width (in px) of the shapes we draw', }, - "field_length": { - "type": int, - "default": 900, - "label": "Field Length", - "tooltip": "Length of the field in cm", + 'field_length': { + 'type': int, + 'default': 900, + 'label': 'Field Length', + 'tooltip': 'Length of the field in cm', }, - "field_width": { - "type": int, - "default": 600, - "label": "Field Width", - "tooltip": "Width of the field in cm", + 'field_width': { + 'type': int, + 'default': 600, + 'label': 'Field Width', + 'tooltip': 'Width of the field in cm', }, - "goal_depth": { - "type": int, - "default": 60, - "label": "Goal Depth", - "tooltip": "Depth of the goal in cm", + 'goal_depth': { + 'type': int, + 'default': 60, + 'label': 'Goal Depth', + 'tooltip': 'Depth of the goal in cm', }, - "goal_width": { - "type": int, - "default": 260, - "label": "Goal Width", - "tooltip": "Width of the goal in cm", + 'goal_width': { + 'type': int, + 'default': 260, + 'label': 'Goal Width', + 'tooltip': 'Width of the goal in cm', }, - "goal_area_length": { - "type": int, - "default": 100, - "label": "Goal Area Length", - "tooltip": "Length of the goal area in cm", + 'goal_area_length': { + 'type': int, + 'default': 100, + 'label': 'Goal Area Length', + 'tooltip': 'Length of the goal area in cm', }, - "goal_area_width": { - "type": int, - "default": 300, - "label": "Goal Area Width", - "tooltip": "Width of the goal area in cm", + 'goal_area_width': { + 'type': int, + 'default': 300, + 'label': 'Goal Area Width', + 'tooltip': 'Width of the goal area in cm', }, - "penalty_mark_distance": { - "type": int, - "default": 150, - "label": "Penalty Mark Distance", - "tooltip": "Distance of the penalty mark from the goal line in cm", + 'penalty_mark_distance': { + 'type': int, + 'default': 150, + 'label': 'Penalty Mark Distance', + 'tooltip': 'Distance of the penalty mark from the goal line in cm', }, - "center_circle_diameter": { - "type": int, - "default": 150, - "label": "Center Circle Diameter", - "tooltip": "Diameter of the center circle in cm", + 'center_circle_diameter': { + 'type': int, + 'default': 150, + 'label': 'Center Circle Diameter', + 'tooltip': 'Diameter of the center circle in cm', }, - "border_strip_width": { - "type": int, - "default": 100, - "label": "Border Strip Width", - "tooltip": "Width of the border strip around the field in cm", + 'border_strip_width': { + 'type': int, + 'default': 100, + 'label': 'Border Strip Width', + 'tooltip': 'Width of the border strip around the field in cm', }, - "penalty_area_length": { - "type": int, - "default": 200, - "label": "Penalty Area Length", - "tooltip": "Length of the penalty area in cm", + 'penalty_area_length': { + 'type': int, + 'default': 200, + 'label': 'Penalty Area Length', + 'tooltip': 'Length of the penalty area in cm', }, - "penalty_area_width": { - "type": int, - "default": 500, - "label": "Penalty Area Width", - "tooltip": "Width of the penalty area in cm", + 'penalty_area_width': { + 'type': int, + 'default': 500, + 'label': 'Penalty Area Width', + 'tooltip': 'Width of the penalty area in cm', }, - "field_feature_size": { - "type": int, - "default": 30, - "label": "Field Feature Size", - "tooltip": "Size of the field features in cm", + 'field_feature_size': { + 'type': int, + 'default': 30, + 'label': 'Field Feature Size', + 'tooltip': 'Size of the field features in cm', }, - "mark_type": { - "type": MarkTypes, - "default": MarkTypes.CROSS, - "label": "Mark Type", - "tooltip": "Type of the marks (penalty mark, center point)", + 'mark_type': { + 'type': MarkTypes, + 'default': MarkTypes.CROSS, + 'label': 'Mark Type', + 'tooltip': 'Type of the marks (penalty mark, center point)', }, - "field_feature_style": { - "type": FieldFeatureStyles, - "default": FieldFeatureStyles.EXACT, - "label": "Field Feature Style", - "tooltip": "Style of the field features", + 'field_feature_style': { + 'type': FieldFeatureStyles, + 'default': FieldFeatureStyles.EXACT, + 'label': 'Field Feature Style', + 'tooltip': 'Style of the field features', }, - "distance_map": { - "type": bool, - "default": False, - "label": "Distance Map", - "tooltip": "Whether or not to draw the distance map", + 'distance_map': { + 'type': bool, + 'default': False, + 'label': 'Distance Map', + 'tooltip': 'Whether or not to draw the distance map', }, - "distance_decay": { - "type": float, - "default": 0.0, - "label": "Distance Decay", - "tooltip": "Exponential decay applied to the distance map", + 'distance_decay': { + 'type': float, + 'default': 0.0, + 'label': 'Distance Decay', + 'tooltip': 'Exponential decay applied to the distance map', }, - "invert": { - "type": bool, - "default": True, - "label": "Invert", - "tooltip": "Invert the final image", + 'invert': { + 'type': bool, + 'default': True, + 'label': 'Invert', + 'tooltip': 'Invert the final image', }, }, ) # Generate Map Button self.save_map_button = ttk.Button( - self.root, text="Save Map", command=self.save_map + self.root, text='Save Map', command=self.save_map ) # Save metadata checkbox self.save_metadata = tk.BooleanVar(value=True) self.save_metadata_checkbox = ttk.Checkbutton( - self.root, text="Save Metadata", variable=self.save_metadata + self.root, text='Save Metadata', variable=self.save_metadata ) # Load and save config buttons self.load_config_button = ttk.Button( - self.root, text="Load Config", command=self.load_config + self.root, text='Load Config', command=self.load_config ) self.save_config_button = ttk.Button( - self.root, text="Save Config", command=self.save_config + self.root, text='Save Config', command=self.save_config ) # Canvas to display the generated map @@ -289,16 +304,16 @@ def __init__(self, root: tk.Tk): def load_config(self): # Prompt the user to select a file (force yaml) file = filedialog.askopenfile( - mode="r", - defaultextension=".yaml", - filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")), + mode='r', + defaultextension='.yaml', + filetypes=(('yaml file', '*.yaml'), ('All Files', '*.*')), ) if file: # Load the config file config = load_config_file(file) if config is None: # Show error box and return if the file is invalid - tk.messagebox.showerror("Error", "Invalid config file") + tk.messagebox.showerror('Error', 'Invalid config file') return # Set the parameters in the gui for parameter_name, parameter_value in config.items(): @@ -313,33 +328,33 @@ def save_config(self): parameters = self.parameter_input.get_parameters() # Open a file dialog to select the file file = filedialog.asksaveasfile( - mode="w", - defaultextension=".yaml", - filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")), + mode='w', + defaultextension='.yaml', + filetypes=(('yaml file', '*.yaml'), ('All Files', '*.*')), ) if file: # Add header - file.write("# Map Generator Config\n") - file.write("# This file was generated by the map generator GUI\n\n") + file.write('# Map Generator Config\n') + file.write('# This file was generated by the map generator GUI\n\n') # Save the parameters in this format: yaml.dump( { - "header": {"version": "1.0", "type": "map_generator_config"}, - "parameters": parameters, + 'header': {'version': '1.0', 'type': 'map_generator_config'}, + 'parameters': parameters, }, file, sort_keys=False, ) - print(f"Saved config to {file.name}") + print(f'Saved config to {file.name}') def save_map(self): file = filedialog.asksaveasfile( - mode="w", - defaultextension=".png", - filetypes=(("png file", "*.png"), ("All Files", "*.*")), + mode='w', + defaultextension='.png', + filetypes=(('png file', '*.png'), ('All Files', '*.*')), ) if file: - print(f"Saving map to {file.name}") + print(f'Saving map to {file.name}') # Generate and save the map parameters = self.parameter_input.get_parameters() @@ -353,28 +368,28 @@ def save_map(self): ) # Save metadata in the same folder as the map metadata_file = os.path.join( - os.path.dirname(file.name), "map_server.yaml" + os.path.dirname(file.name), 'map_server.yaml' ) - with open(metadata_file, "w") as f: + with open(metadata_file, 'w') as f: yaml.dump(metadata, f, sort_keys=False) - print(f"Saved metadata to {metadata_file}") + print(f'Saved metadata to {metadata_file}') # Show success box and ask if we want to open it with the default image viewer if tk.messagebox.askyesno( - "Success", "Map saved successfully. Do you want to open it?" + 'Success', 'Map saved successfully. Do you want to open it?' ): import platform import subprocess - if platform.system() == "Windows": - subprocess.Popen(["start", file.name], shell=True) - elif platform.system() == "Darwin": - subprocess.Popen(["open", file.name]) + if platform.system() == 'Windows': + subprocess.Popen(['start', file.name], shell=True) + elif platform.system() == 'Darwin': + subprocess.Popen(['open', file.name]) else: - subprocess.Popen(["xdg-open", file.name]) + subprocess.Popen(['xdg-open', file.name]) else: # Show error box - tk.messagebox.showerror("Error", "Could not save map to file") + tk.messagebox.showerror('Error', 'Could not save map to file') def update_map(self, *args): # Generate and display the map on the canvas @@ -403,5 +418,5 @@ def main(): root.mainloop() -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/soccer_field_map_generator/soccer_field_map_generator/tooltip.py b/soccer_field_map_generator/soccer_field_map_generator/tooltip.py index b92970f..c71ac0a 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/tooltip.py +++ b/soccer_field_map_generator/soccer_field_map_generator/tooltip.py @@ -36,9 +36,9 @@ def __init__( self, widget, *, - bg="#FFFFEA", + bg='#FFFFEA', pad=(5, 3, 5, 3), - text="widget info", + text='widget info', waittime=400, wraplength=250 ): @@ -46,9 +46,9 @@ def __init__( self.wraplength = wraplength # in pixels, originally 180 self.widget = widget self.text = text - self.widget.bind("", self.onEnter) - self.widget.bind("", self.onLeave) - self.widget.bind("", self.onLeave) + self.widget.bind('', self.onEnter) + self.widget.bind('', self.onLeave) + self.widget.bind('', self.onLeave) self.bg = bg self.pad = pad self.id = None @@ -142,7 +142,7 @@ def tip_pos_calculator(widget, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): x, y = tip_pos_calculator(widget, label) - self.tw.wm_geometry("+%d+%d" % (x, y)) + self.tw.wm_geometry('+%d+%d' % (x, y)) def hide(self): tw = self.tw From 4fa52265a7d7e53c4f4582292c08123d9e3ce29c Mon Sep 17 00:00:00 2001 From: Florian Vahl Date: Fri, 22 Dec 2023 00:49:09 +0100 Subject: [PATCH 6/8] Add line after class --- soccer_field_map_generator/soccer_field_map_generator/gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/soccer_field_map_generator/soccer_field_map_generator/gui.py b/soccer_field_map_generator/soccer_field_map_generator/gui.py index b6eec78..750e391 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/gui.py +++ b/soccer_field_map_generator/soccer_field_map_generator/gui.py @@ -34,6 +34,7 @@ class MapGeneratorParamInput(tk.Frame): + def __init__( self, parent, update_hook: callable, parameter_definitions: dict[str, dict] ): From a8ec005ea1ba878500cdae564ff95a6c96ac8cbe Mon Sep 17 00:00:00 2001 From: Florian Vahl Date: Fri, 22 Dec 2023 00:53:58 +0100 Subject: [PATCH 7/8] newline in class --- soccer_field_map_generator/soccer_field_map_generator/gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/soccer_field_map_generator/soccer_field_map_generator/gui.py b/soccer_field_map_generator/soccer_field_map_generator/gui.py index 750e391..8024ddb 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/gui.py +++ b/soccer_field_map_generator/soccer_field_map_generator/gui.py @@ -103,6 +103,7 @@ def get_parameter(self, parameter_name): class MapGeneratorGUI: + def __init__(self, root: tk.Tk): # Set ttk theme s = ttk.Style() From 34c3357e6899dff93c6dd3539c380c9ecf1315af Mon Sep 17 00:00:00 2001 From: Florian Vahl Date: Fri, 22 Dec 2023 01:01:55 +0100 Subject: [PATCH 8/8] Remove whitespace --- soccer_field_map_generator/soccer_field_map_generator/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soccer_field_map_generator/soccer_field_map_generator/gui.py b/soccer_field_map_generator/soccer_field_map_generator/gui.py index 8024ddb..6565020 100644 --- a/soccer_field_map_generator/soccer_field_map_generator/gui.py +++ b/soccer_field_map_generator/soccer_field_map_generator/gui.py @@ -103,7 +103,7 @@ def get_parameter(self, parameter_name): class MapGeneratorGUI: - + def __init__(self, root: tk.Tk): # Set ttk theme s = ttk.Style()