f^JZureFbUK
z9X>xC&+`N8pea%tYip#vFAW>^v3p+X+uznuf^>|xPwZpi?&d{mW9^Hy!@i(Ky5;UT
z;;CgRA@<0zz6>Quhuld=bQ~7`Taj}Usg1QS(hmEA8qH1k{P5fymJ)}9*cmg|p#zr(V0VZqKwF*J*nu;g!@s548_`L>+t;VY2Bl3cs@qum-omI`)5@C
z@IYRSZnnSnOn$e&_E47VMx2eo+
{06^x?t57$tIO
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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!Sgb8e8IhxCywWaE{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
[](../../actions/workflows/build_and_test_humble.yaml?query=branch:rolling)
[](../../actions/workflows/build_and_test_iron.yaml?query=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:
+
+
+
+### 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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