Skip to content
This repository was archived by the owner on Jan 23, 2024. It is now read-only.

Add map gen gui #144

Merged
merged 14 commits into from
Dec 28, 2023
Prev Previous commit
Next Next commit
Use black
Flova committed Dec 19, 2023
commit 588613de930cfa1dc003c406ab4b4cc20dddf78c
33 changes: 21 additions & 12 deletions soccer_field_map_generator/soccer_field_map_generator/cli.py
Original file line number Diff line number Diff line change
@@ -4,18 +4,25 @@
import yaml
import argparse

from soccer_field_map_generator.generator import generate_map_image, generate_metadata, load_config_file
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')
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
@@ -24,7 +31,7 @@ def main():
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
@@ -49,10 +56,12 @@ def main():
# 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:
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__':
if __name__ == "__main__":
main()
654 changes: 453 additions & 201 deletions soccer_field_map_generator/soccer_field_map_generator/generator.py

Large diffs are not rendered by default.

378 changes: 212 additions & 166 deletions soccer_field_map_generator/soccer_field_map_generator/gui.py
Original file line number Diff line number Diff line change
@@ -7,12 +7,21 @@
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.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]):
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
@@ -26,7 +35,9 @@ def __init__(self, parent, update_hook: callable, parameter_definitions: dict[st
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)
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)
@@ -50,20 +61,26 @@ def __init__(self, parent, update_hook: callable, parameter_definitions: dict[st
# Add ui elements to the dict
self.parameter_ui_elements[parameter_name] = {
"label": label,
"ui_element": ui_element
"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")
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()}
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()
@@ -73,7 +90,7 @@ 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
@@ -83,154 +100,168 @@ def __init__(self, root: tk.Tk):
# Create GUI elements

# Title
self.title = ttk.Label(self.root, text="Soccer Map Generator", font=("TkDefaultFont", 16))
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"
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",
},
},
"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.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)
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)
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)
@@ -258,9 +289,10 @@ def __init__(self, root: tk.Tk):
def load_config(self):
# Prompt the user to select a file (force yaml)
file = filedialog.askopenfile(
mode='r',
mode="r",
defaultextension=".yaml",
filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")))
filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")),
)
if file:
# Load the config file
config = load_config_file(file)
@@ -270,7 +302,9 @@ def load_config(self):
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)
self.parameter_input.parameter_values[parameter_name].set(
parameter_value
)
# Update the map
self.update_map()

@@ -279,28 +313,31 @@ def save_config(self):
parameters = self.parameter_input.get_parameters()
# Open a file dialog to select the file
file = filedialog.asksaveasfile(
mode='w',
mode="w",
defaultextension=".yaml",
filetypes=(("yaml file", "*.yaml"), ("All Files", "*.*")))
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"
yaml.dump(
{
"header": {"version": "1.0", "type": "map_generator_config"},
"parameters": parameters,
},
"parameters": parameters
}, file, sort_keys=False)
file,
sort_keys=False,
)
print(f"Saved config to {file.name}")

def save_map(self):
file = filedialog.asksaveasfile(
mode='w',
mode="w",
defaultextension=".png",
filetypes=(("png file", "*.png"), ("All Files", "*.*")))
filetypes=(("png file", "*.png"), ("All Files", "*.*")),
)
if file:
print(f"Saving map to {file.name}")

@@ -311,18 +348,24 @@ def save_map(self):
# Save metadata
if self.save_metadata.get():
# Save the metadata in this format:
metadata = generate_metadata(parameters, os.path.basename(file.name))
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")
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?"):
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":
@@ -345,7 +388,10 @@ 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.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
@@ -357,5 +403,5 @@ def main():
root.mainloop()


if __name__ == '__main__':
if __name__ == "__main__":
main()
62 changes: 30 additions & 32 deletions soccer_field_map_generator/soccer_field_map_generator/tooltip.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@


class Tooltip:
'''
"""
It creates a tooltip for a given widget as the mouse goes on it.
see:
@@ -30,16 +30,18 @@ class Tooltip:
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):

"""

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
@@ -70,16 +72,15 @@ def unschedule(self):
self.widget.after_cancel(id_)

def show(self):
def tip_pos_calculator(widget, label,
*,
tip_delta=(10, 5), pad=(5, 3, 5, 3)):

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])
width, height = (
pad[0] + label.winfo_reqwidth() + pad[2],
pad[1] + label.winfo_reqheight() + pad[3],
)

mouse_x, mouse_y = w.winfo_pointerxy()

@@ -96,7 +97,6 @@ def tip_pos_calculator(widget, label,
offscreen = (x_delta, y_delta) != (0, 0)

if offscreen:

if x_delta:
x1 = mouse_x - tip_delta[0] - width

@@ -126,20 +126,18 @@ def tip_pos_calculator(widget, label,
# 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 = 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)