├── scripts ├── __pycache__ │ ├── gui.cpython-311.pyc │ ├── imageConverter.cpython-311.pyc │ ├── markFieldLoops.cpython-311.pyc │ ├── imageToCoordinates.cpython-311.pyc │ ├── processFieldLoops.cpython-311.pyc │ ├── simplifyFieldLoops.cpython-311.pyc │ └── finalizeFieldCoordinates.cpython-311.pyc ├── visualizeFieldLoopXML.py ├── visualizeFieldAllXML.py ├── imageConverter.py ├── finalizeFieldCoordinates.py ├── imageToCoordinates.py ├── markFieldLoops.py ├── processFieldCoordinates.py ├── processFieldLoops.py ├── simplifyFieldLoops.py ├── theme.json └── gui.py ├── LICENSE ├── README.md └── main.py /scripts/__pycache__/gui.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/gui.cpython-311.pyc -------------------------------------------------------------------------------- /scripts/__pycache__/imageConverter.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/imageConverter.cpython-311.pyc -------------------------------------------------------------------------------- /scripts/__pycache__/markFieldLoops.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/markFieldLoops.cpython-311.pyc -------------------------------------------------------------------------------- /scripts/__pycache__/imageToCoordinates.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/imageToCoordinates.cpython-311.pyc -------------------------------------------------------------------------------- /scripts/__pycache__/processFieldLoops.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/processFieldLoops.cpython-311.pyc -------------------------------------------------------------------------------- /scripts/__pycache__/simplifyFieldLoops.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/simplifyFieldLoops.cpython-311.pyc -------------------------------------------------------------------------------- /scripts/__pycache__/finalizeFieldCoordinates.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PixelFarm1/FS25_ImageToFields/HEAD/scripts/__pycache__/finalizeFieldCoordinates.cpython-311.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 PixelFarm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/visualizeFieldLoopXML.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import matplotlib.pyplot as plt 3 | import argparse 4 | 5 | def parse_field_coordinates(file_path, field_id): 6 | """ 7 | Parse the XML file to extract coordinates for a specific field ID. 8 | """ 9 | tree = ET.parse(file_path) 10 | root = tree.getroot() 11 | 12 | for field in root.findall("Field"): 13 | if field.attrib.get("ID") == field_id: 14 | loops = [] 15 | for loop in field.findall("Loop"): 16 | coordinates = [ 17 | (float(coord.attrib["X"]), float(coord.attrib["Y"])) 18 | for coord in loop.findall("coordinate") 19 | ] 20 | loops.append(coordinates) 21 | return loops 22 | return None 23 | 24 | def visualize_coordinates(loops, field_id): 25 | """ 26 | Visualize the coordinates of the given loops. 27 | """ 28 | plt.figure(figsize=(10, 8)) 29 | for loop in loops: 30 | x, y = zip(*loop) 31 | plt.plot(x, y, label=f"Loop in Field {field_id}") 32 | plt.xlabel("X Coordinate") 33 | plt.ylabel("Y Coordinate") 34 | plt.title(f"Visualization of Field ID: {field_id}") 35 | plt.legend() 36 | plt.grid(True) 37 | plt.show() 38 | 39 | def main(): 40 | parser = argparse.ArgumentParser(description="Visualize field loops from XML.") 41 | parser.add_argument("file", help="Path to the XML file.") 42 | parser.add_argument("field_id", help="Field ID to visualize.") 43 | args = parser.parse_args() 44 | 45 | loops = parse_field_coordinates(args.file, args.field_id) 46 | if loops: 47 | visualize_coordinates(loops, args.field_id) 48 | else: 49 | print(f"No data found for Field ID: {args.field_id}") 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /scripts/visualizeFieldAllXML.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import matplotlib.pyplot as plt 3 | import random 4 | 5 | def parse_all_field_coordinates_with_center_flipped_plane(file_path): 6 | """ 7 | Parse the XML file to extract coordinates for all fields with respect to their center, 8 | flipping the entire Y-plane while preserving polygon orientation. 9 | """ 10 | tree = ET.parse(file_path) 11 | root = tree.getroot() 12 | 13 | fields = {} 14 | centers = {} 15 | for field in root.findall("Field"): 16 | field_id = field.attrib.get("ID") 17 | center_x = float(field.attrib.get("X")) 18 | center_y = -float(field.attrib.get("Y")) # Flip the Y-plane for the center 19 | 20 | coordinates = [ 21 | (center_x + float(coord.attrib["X"]), center_y - float(coord.attrib["Y"])) # Flip Y-plane for center and coords 22 | for coord in field.findall("coordinate") 23 | ] 24 | fields[field_id] = coordinates 25 | centers[field_id] = (center_x, center_y) 26 | return fields, centers 27 | 28 | def visualize_all_fields_with_labels(file_path): 29 | """ 30 | Visualize the coordinates of all fields relative to their center, each in a unique color, 31 | with the entire Y-plane flipped and polygon orientation preserved. Field centers are labeled with IDs. 32 | """ 33 | fields, centers = parse_all_field_coordinates_with_center_flipped_plane(file_path) 34 | plt.figure(figsize=(12, 12)) 35 | 36 | for field_id, coords in fields.items(): 37 | # Generate a random color for each field 38 | color = [random.random() for _ in range(3)] 39 | x, y = zip(*coords) 40 | plt.plot(x, y, label=f"Field {field_id}", color="black") 41 | # Mark the center point of the field and label it 42 | center_x, center_y = centers[field_id] 43 | #plt.scatter(center_x, center_y, color=color, edgecolor='black', zorder=5) 44 | plt.text(center_x, center_y, f"ID {field_id}", fontsize=9, ha='center', va='center', zorder=10) 45 | 46 | plt.xlabel("X Coordinate") 47 | plt.ylabel("Y Coordinate (Flipped Plane, Preserved Orientation)") 48 | plt.title("Visualization of All Fields with Flipped Y-Plane and Labels") 49 | plt.grid(True) 50 | plt.show() 51 | 52 | # Path to the XML file 53 | file_path = 'C:/Users/Willis/Desktop/FS25_ImageToField/FS25_ImageToField/output/final_field_coordinates.xml' 54 | 55 | # Generate the visualization 56 | visualize_all_fields_with_labels(file_path) 57 | -------------------------------------------------------------------------------- /scripts/imageConverter.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import os 4 | 5 | class imageConvert: 6 | def __init__(self): 7 | pass 8 | 9 | def process(self, input_file, output_dir): 10 | """ 11 | Process the input file to create a white mask with unique island coloring. 12 | 13 | Args: 14 | input_file (str): Path to the input grayscale image. 15 | output_dir (str): Directory to save the processed output. 16 | 17 | Returns: 18 | str: Path to the saved output file. 19 | 20 | Raises: 21 | Exception: If the input file cannot be processed. 22 | """ 23 | print("Image analysis: Starting process...") 24 | 25 | # Verify input file exists 26 | if not os.path.exists(input_file): 27 | raise FileNotFoundError(f"Image analysis: Input file '{input_file}' not found.") 28 | 29 | # Load the image in grayscale 30 | print(f"Image analysis: Loading image from {input_file}") 31 | image = cv2.imread(input_file, cv2.IMREAD_GRAYSCALE) 32 | if image is None: 33 | raise ValueError(f"Image analysis: Failed to load input image from {input_file}") 34 | 35 | # Threshold the image to separate white islands from the background 36 | print("Image analysis: Thresholding image...") 37 | _, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY) 38 | 39 | # Find contours of white islands 40 | print("Image analysis: Finding contours...") 41 | contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 42 | 43 | # Create a blank image for coloring the white islands 44 | colored_image = np.zeros((image.shape[0], image.shape[1], 3), dtype=np.uint8) 45 | 46 | # Assign a unique value to the red channel of each white island 47 | print(f"Image analysis: Coloring {len(contours)} white islands...") 48 | for i, contour in enumerate(contours): 49 | # Get the red channel value for the current island 50 | red_value = i + 1 51 | 52 | # Create a mask for the current island contour 53 | mask = np.zeros_like(image) 54 | cv2.drawContours(mask, [contour], -1, 255, thickness=cv2.FILLED) 55 | 56 | # Color all the pixels within the contour 57 | colored_image[:, :, 2] = np.where((mask > 0) & (image == 255), red_value, colored_image[:, :, 2]) 58 | 59 | # Save the colored image as an 8bpc RGB PNG 60 | output_file = os.path.join(output_dir, "processed_image.png") 61 | print(f"Image analysis: Saving processed image to {output_file}") 62 | cv2.imwrite(output_file, colored_image, [cv2.IMWRITE_PNG_COMPRESSION, 0]) 63 | 64 | print("Image analysis: Processing completed.") 65 | return output_file 66 | -------------------------------------------------------------------------------- /scripts/finalizeFieldCoordinates.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import os 3 | 4 | class FinalizeFieldCoordinates: 5 | def process(self, input_file, output_dir): 6 | """ 7 | Processes the marked field coordinates to finalize the XML structure. 8 | 9 | Args: 10 | input_file (str): Path to the input XML file. 11 | output_dir (str): Directory to save the processed XML file. 12 | 13 | Returns: 14 | str: Path to the saved processed XML file. 15 | """ 16 | def process_field(field): 17 | """Process a single field and finalize its coordinates.""" 18 | field_id = field.get("ID") 19 | x_attr = field.get("X", "0") # Preserve X attribute 20 | y_attr = field.get("Y", "0") # Preserve Y attribute 21 | 22 | print(f"Processing Field ID: {field_id}") 23 | output_field = ET.SubElement(output_root, "Field", {"ID": field_id, "X": x_attr, "Y": y_attr}) 24 | 25 | # Extract Loop 1 and all other loops 26 | loop1 = field.find("./Loop[@ID='1']") 27 | if loop1 is None: 28 | print(f"No Loop ID='1' found in Field ID={field_id}. Skipping.") 29 | return 30 | 31 | merge_points = [] 32 | other_loops = {} 33 | 34 | # Collect merge points and other loops 35 | for loop in field.findall("Loop"): 36 | loop_id = loop.get("ID") 37 | if loop_id == "1": 38 | for coordinate in loop.findall("coordinate"): 39 | merge_id = coordinate.get("mergeID") 40 | if merge_id: 41 | merge_points.append((merge_id, coordinate)) 42 | print(f"Found merging point in Field ID={field_id}, Loop ID={loop_id}: mergeID={merge_id}") 43 | else: 44 | other_loops[loop_id] = [coord.attrib for coord in loop.findall("coordinate")] 45 | 46 | # Rearrange coordinates in Loop 1 with merging logic 47 | ordered_coords = [] 48 | for coord in loop1.findall("coordinate"): 49 | ordered_coords.append(coord.attrib) 50 | if coord.get("mergeID"): 51 | merge_id = coord.get("mergeID") 52 | if merge_id in other_loops: 53 | print(f"Inserting coordinates from Loop ID={merge_id} into Loop ID=1 for Field ID={field_id}") 54 | # Insert matching loop coordinates after this merging point 55 | ordered_coords.extend(other_loops[merge_id]) 56 | del other_loops[merge_id] # Remove merged loop from other_loops 57 | 58 | # Add ordered coordinates to output 59 | for coord in ordered_coords: 60 | ET.SubElement(output_field, "coordinate", coord) 61 | 62 | # Define output file path 63 | output_file = os.path.join(output_dir, "final_field_coordinates.xml") 64 | 65 | try: 66 | # Load XML file 67 | print(f"Loading XML file: {input_file}") 68 | tree = ET.parse(input_file) 69 | root = tree.getroot() 70 | 71 | # Initialize output XML structure 72 | output_root = ET.Element("Fields") 73 | 74 | # Process each field 75 | for field in root.findall("Field"): 76 | process_field(field) 77 | 78 | # Write to output XML 79 | print(f"Writing output XML to: {output_file}") 80 | tree = ET.ElementTree(output_root) 81 | tree.write(output_file, encoding="utf-8", xml_declaration=True) 82 | print("Processing completed successfully.") 83 | 84 | return output_file 85 | 86 | except Exception as e: 87 | print(f"An error occurred while processing the XML: {e}") 88 | raise 89 | -------------------------------------------------------------------------------- /scripts/imageToCoordinates.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import xml.etree.ElementTree as ET 4 | import os 5 | 6 | class createCoordinates: 7 | def process(self, input_image_path, output_dir, dem_size): 8 | """ 9 | Extract field coordinates with 3D center from an image and save to "coordinates1.xml". 10 | 11 | Args: 12 | input_image_path (str): Path to the input image file. 13 | output_dir (str): Directory to save the output XML file. 14 | dem_size (int): Size of the DEM for coordinate calculations. 15 | 16 | Returns: 17 | str: Path to the saved XML file. 18 | 19 | Raises: 20 | Exception: If the input image cannot be loaded or processing fails. 21 | """ 22 | print("Creating coordinates: Starting to process the image...") 23 | 24 | # Verify the input image exists and is readable 25 | if not os.path.exists(input_image_path): 26 | raise FileNotFoundError(f"Input image file does not exist: {input_image_path}") 27 | 28 | # Ensure output directory exists 29 | print(f"Creating coordinates: Ensuring directory exists: {output_dir}") 30 | os.makedirs(output_dir, exist_ok=True) 31 | 32 | # Construct the output XML file path 33 | output_xml_path = os.path.join(output_dir, "coordinates1.xml") 34 | print(f"Creating coordinates: Output XML path set to {output_xml_path}") 35 | 36 | # Verify directory permissions 37 | if not os.access(output_dir, os.W_OK): 38 | raise PermissionError(f"Directory is not writable: {output_dir}") 39 | 40 | 41 | 42 | # Load the image 43 | image = cv2.imread(input_image_path) 44 | if image is None: 45 | print(f"Creating coordinates: Failed to load image from {input_image_path}.") 46 | raise FileNotFoundError(f"Image file not found or unreadable: {input_image_path}") 47 | 48 | print(f"Creating coordinates: Image loaded successfully. Processing dimensions and red channel...") 49 | height, width, _ = image.shape 50 | ratio = width / dem_size 51 | red_channel = image[:, :, 2] 52 | unique_red_values = sorted(set(np.unique(red_channel)) - {0}) 53 | 54 | root = ET.Element("Fields") 55 | print(f"Creating coordinates: Detected {len(unique_red_values)} unique field regions to process.") 56 | 57 | for idx, red_value in enumerate(unique_red_values, start=1): 58 | print(f"Creating coordinates: Processing field {idx}/{len(unique_red_values)} with red value {red_value}...") 59 | mask = (red_channel == red_value).astype(np.uint8) * 255 60 | contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) 61 | 62 | field_coordinates = [ 63 | (round((point[0][0] - (width // 2)) / ratio, 2), 64 | round(((point[0][1] - (height // 2)) / ratio), 2)) # Flip Y-axis 65 | for contour in contours for point in contour 66 | ] 67 | 68 | M = cv2.moments(mask) 69 | center_x, center_y = (0, 0) 70 | if M["m00"] != 0: 71 | center_x = round((int(M["m10"] / M["m00"]) - (width // 2)) / ratio, 2) 72 | center_y = round(((int(M["m01"] / M["m00"]) - (height // 2)) / ratio), 2) # Flip Y-axis 73 | 74 | adjusted_coordinates = [ 75 | (round(coord[0] - center_x, 2), round(coord[1] - center_y, 2)) 76 | for coord in field_coordinates 77 | ] 78 | 79 | field_element = ET.SubElement(root, "Field", ID=str(int(red_value)), 80 | X=str(center_x), Y=str(center_y)) 81 | for coord in adjusted_coordinates: 82 | ET.SubElement(field_element, "coordinate", X=str(coord[0]), Y=str(coord[1])) 83 | 84 | tree = ET.ElementTree(root) 85 | print(f"Creating coordinates: Saving XML to: {output_xml_path}") 86 | with open(output_xml_path, "wb") as f: 87 | tree.write(f, encoding="utf-8", xml_declaration=True) 88 | 89 | print(f"Creating coordinates: Processing complete. XML file saved to {output_xml_path}.") 90 | return output_xml_path 91 | -------------------------------------------------------------------------------- /scripts/markFieldLoops.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import math 3 | import os 4 | 5 | class MarkFieldLoops: 6 | def process(self, input_file, output_dir): 7 | """ 8 | Marks and processes field loops in the input XML file. 9 | 10 | Args: 11 | input_file (str): Path to the input XML file. 12 | output_dir (str): Directory to save the processed XML file. 13 | 14 | Returns: 15 | str: Path to the saved processed XML file. 16 | """ 17 | def calculate_distance(x1, y1, x2, y2): 18 | """Calculate Euclidean distance between two points.""" 19 | return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) 20 | 21 | def process_field(field): 22 | """Process a single field and mark the loops.""" 23 | x_attr = field.attrib.get("X", "0") # Preserve X attribute 24 | y_attr = field.attrib.get("Y", "0") # Preserve Y attribute 25 | 26 | loops = field.findall("Loop") 27 | main_loop = [ 28 | (float(coord.attrib["X"]), float(coord.attrib["Y"])) 29 | for coord in loops[0].findall("coordinate") 30 | ] 31 | 32 | print(f"Processing Field ID: {field.attrib['ID']}, Main Loop has {len(main_loop)} coordinates.") 33 | 34 | for loop in loops[1:]: 35 | other_coords = [ 36 | (float(coord.attrib["X"]), float(coord.attrib["Y"])) 37 | for coord in loop.findall("coordinate") 38 | ] 39 | 40 | # Find the closest coordinate in the main loop 41 | min_distance = float("inf") 42 | min_main_index = -1 43 | min_other_index = -1 44 | 45 | for main_index, (main_x, main_y) in enumerate(main_loop): 46 | for other_index, (other_x, other_y) in enumerate(other_coords): 47 | distance = calculate_distance(main_x, main_y, other_x, other_y) 48 | if distance < min_distance: 49 | min_distance = distance 50 | min_main_index = main_index 51 | min_other_index = other_index 52 | 53 | # Mark the closest coordinate in the main loop 54 | merge_id = loop.attrib["ID"] 55 | main_coord = loops[0].findall("coordinate")[min_main_index] 56 | main_coord.set("mergeID", merge_id) 57 | 58 | print(f"Marking Main Loop Coord: {main_coord.attrib} with mergeID {merge_id}") 59 | 60 | # Duplicate the marked coordinate 61 | duplicate_coord = ET.Element("coordinate", main_coord.attrib) 62 | loops[0].insert(min_main_index + 1, duplicate_coord) 63 | 64 | # Reorder the loop to start at the closest coordinate 65 | reordered_coords = other_coords[min_other_index:] + other_coords[:min_other_index] 66 | for i, coord_elem in enumerate(loop.findall("coordinate")): 67 | coord_elem.set("X", str(reordered_coords[i][0])) 68 | coord_elem.set("Y", str(reordered_coords[i][1])) 69 | 70 | print(f"Reordered Loop ID {merge_id} to start at index {min_other_index}.") 71 | 72 | # Append the first coordinate to close the loop 73 | first_coord = loop.findall("coordinate")[0] 74 | closing_coord = ET.Element("coordinate", first_coord.attrib) 75 | loop.append(closing_coord) 76 | 77 | print(f"Loop ID {merge_id} closed with coordinate {closing_coord.attrib}.") 78 | 79 | # Reassign the preserved X and Y attributes back to the tag 80 | field.set("X", x_attr) 81 | field.set("Y", y_attr) 82 | 83 | # Load the XML file 84 | print(f"Loading XML file: {input_file}") 85 | tree = ET.parse(input_file) 86 | root = tree.getroot() 87 | 88 | # Process each field 89 | for field in root.findall("Field"): 90 | process_field(field) 91 | 92 | # Save the processed XML file 93 | output_file = os.path.join(output_dir, "field_coordinates_marked.xml") 94 | tree.write(output_file, encoding="utf-8", xml_declaration=True) 95 | print(f"Processed XML saved to {output_file}.") 96 | return output_file 97 | -------------------------------------------------------------------------------- /scripts/processFieldCoordinates.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import math 3 | import os 4 | 5 | class createLoops: 6 | def process(self, input_file, output_dir, threshold): 7 | """ 8 | Process field coordinates in the input XML file and save the cleaned version. 9 | 10 | Args: 11 | input_file (str): Path to the input XML file. 12 | output_dir (str): Directory to save the output XML file. 13 | threshold (float): Distance threshold for segmenting loops. 14 | 15 | Returns: 16 | str: Path to the saved output XML file. 17 | 18 | Raises: 19 | Exception: If any error occurs during processing. 20 | """ 21 | def euclidean_distance(coord1, coord2): 22 | """Calculate Euclidean distance between two coordinates.""" 23 | try: 24 | return math.sqrt((coord2[0] - coord1[0]) ** 2 + (coord2[1] - coord1[1]) ** 2) 25 | except TypeError: 26 | raise ValueError(f"Invalid coordinates: coord1={coord1}, coord2={coord2}") 27 | 28 | def close_loop(loop): 29 | """Ensure the loop starts and ends at the same coordinate.""" 30 | if loop[0] != loop[-1]: 31 | loop.append(loop[0]) 32 | return loop 33 | 34 | def integrate_loops(base_loop, other_loops): 35 | """ 36 | Integrate other loops into the base loop: 37 | - Loops attach only to the main loop. 38 | - Rearrange coordinates of loops such that the beginning and end 39 | of each loop surround the attachment point in the main loop. 40 | """ 41 | for loop in other_loops: 42 | # Find the closest point in base_loop to the start of the current loop 43 | start_coord = loop[0] 44 | closest_idx = min( 45 | range(len(base_loop)), 46 | key=lambda i: euclidean_distance(base_loop[i], start_coord) 47 | ) 48 | # Rearrange the loop to ensure closure 49 | closed_loop = close_loop(loop) 50 | # Integrate the loop into the base loop with reordered coordinates 51 | base_loop = ( 52 | base_loop[:closest_idx + 1] 53 | + closed_loop 54 | + [base_loop[closest_idx]] # Repeat the attachment point for closure 55 | + base_loop[closest_idx + 1:] 56 | ) 57 | return base_loop 58 | 59 | # Output file path 60 | output_file = os.path.join(output_dir, "processed_field_coordinates.xml") 61 | 62 | # Parse the input XML file 63 | try: 64 | tree = ET.parse(input_file) 65 | root = tree.getroot() 66 | except ET.ParseError as e: 67 | raise ValueError(f"Failed to parse XML file: {e}") 68 | 69 | for field in root.findall("Field"): 70 | field_id = field.attrib.get('ID', 'unknown') 71 | x_attr = field.attrib.get('X', None) # Preserve X attribute 72 | y_attr = field.attrib.get('Y', None) # Preserve Y attribute 73 | 74 | coordinates = [] 75 | for coord in field.findall("coordinate"): 76 | try: 77 | x = float(coord.attrib['X']) 78 | y = float(coord.attrib['Y']) 79 | coordinates.append((x, y)) 80 | except (KeyError, ValueError): 81 | continue 82 | 83 | if not coordinates: 84 | continue 85 | 86 | # Segment coordinates into loops 87 | loops = [] 88 | current_loop = [coordinates[0]] 89 | for i in range(1, len(coordinates)): 90 | if euclidean_distance(coordinates[i - 1], coordinates[i]) > threshold: 91 | loops.append(close_loop(current_loop)) 92 | current_loop = [] 93 | current_loop.append(coordinates[i]) 94 | loops.append(close_loop(current_loop)) 95 | 96 | # Merge loops into a single loop 97 | base_loop = loops[0] 98 | other_loops = loops[1:] 99 | integrated_loop = integrate_loops(base_loop, other_loops) 100 | 101 | # Clear and restore coordinates in the original structure 102 | field.clear() # Clear existing children but preserve attributes 103 | if field_id != 'unknown': 104 | field.set('ID', field_id) 105 | if x_attr: 106 | field.set('X', x_attr) 107 | if y_attr: 108 | field.set('Y', y_attr) 109 | 110 | for x, y in integrated_loop: 111 | ET.SubElement(field, "coordinate", X=str(x), Y=str(y)) 112 | 113 | # Write the final cleaned XML to the output file 114 | tree.write(output_file, encoding="utf-8", xml_declaration=True) 115 | return output_file 116 | -------------------------------------------------------------------------------- /scripts/processFieldLoops.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import math 3 | import os 4 | 5 | 6 | class ProcessFieldLoops: 7 | def process(self, input_file, output_dir, threshold=10): 8 | """ 9 | Process field loops in the input XML file and save the updated XML. 10 | 11 | Args: 12 | input_file (str): Path to the input XML file. 13 | output_dir (str): Directory to save the output XML file. 14 | threshold (float): Distance threshold for segmenting loops. 15 | 16 | Returns: 17 | str: Path to the saved output XML file. 18 | 19 | Raises: 20 | Exception: If any error occurs during processing. 21 | """ 22 | def euclidean_distance(coord1, coord2): 23 | """Calculate Euclidean distance between two coordinates.""" 24 | return math.sqrt((coord2[0] - coord1[0]) ** 2 + (coord2[1] - coord1[1]) ** 2) 25 | 26 | def close_loop(loop): 27 | """Ensure the loop starts and ends at the same coordinate.""" 28 | if loop and loop[0] != loop[-1]: 29 | loop.append(loop[0]) 30 | return loop 31 | 32 | def rearrange_loops(base_loop, other_loops): 33 | """Rearrange loops so loops with ID > 1 are adjacent to the nearest point in base_loop.""" 34 | print(f"Rearranging {len(other_loops)} additional loops relative to the base loop.") 35 | rearranged = [] 36 | for loop in other_loops: 37 | closest_base = min( 38 | base_loop, 39 | key=lambda base: min(euclidean_distance(base, coord) for coord in loop) 40 | ) 41 | rearranged.append((closest_base, loop)) 42 | rearranged.sort(key=lambda x: x[0]) # Sort by proximity to base_loop 43 | return [item[1] for item in rearranged] 44 | 45 | # Define the output file path 46 | output_file = os.path.join(output_dir, "field_loops.xml") 47 | print(f"Starting to process field loops from {input_file}") 48 | 49 | # Parse the input XML file 50 | try: 51 | tree = ET.parse(input_file) 52 | root = tree.getroot() 53 | print(f"Successfully parsed XML file: {input_file}") 54 | except ET.ParseError as e: 55 | print(f"Failed to parse XML file: {e}") 56 | raise ValueError(f"Failed to parse XML file: {e}") 57 | 58 | for field in root.findall("Field"): 59 | field_id = field.attrib.get('ID', 'unknown') 60 | x_attr = field.attrib.get('X', '0') 61 | y_attr = field.attrib.get('Y', '0') 62 | 63 | print(f"Processing Field ID: {field_id}") 64 | 65 | coordinates = [] 66 | for coord in field.findall("coordinate"): 67 | try: 68 | x = float(coord.attrib['X']) 69 | y = float(coord.attrib['Y']) 70 | coordinates.append((x, y)) 71 | except (KeyError, ValueError) as e: 72 | print(f"Skipping invalid coordinate: {e}") 73 | 74 | if not coordinates: 75 | print(f"No valid coordinates for Field ID: {field_id}") 76 | continue 77 | 78 | # Segment coordinates into loops 79 | print(f"Segmenting coordinates into loops for Field ID: {field_id}") 80 | loops = [] 81 | current_loop = [coordinates[0]] 82 | for i in range(1, len(coordinates)): 83 | if euclidean_distance(coordinates[i - 1], coordinates[i]) > threshold: 84 | loops.append(close_loop(current_loop)) 85 | current_loop = [] 86 | current_loop.append(coordinates[i]) 87 | loops.append(close_loop(current_loop)) 88 | print(f"Segmented {len(loops)} loops for Field ID: {field_id}") 89 | 90 | # Rearrange loops 91 | base_loop = loops[0] 92 | other_loops = loops[1:] 93 | rearranged_loops = rearrange_loops(base_loop, other_loops) 94 | print(f"Rearranged loops for Field ID: {field_id}") 95 | 96 | # Update field with segmented and rearranged loops 97 | field.clear() 98 | field.attrib['ID'] = field_id 99 | field.attrib['X'] = x_attr 100 | field.attrib['Y'] = y_attr 101 | 102 | for i, loop in enumerate([base_loop] + rearranged_loops, start=1): 103 | loop_element = ET.SubElement(field, "Loop", ID=str(i)) 104 | for x, y in loop: 105 | ET.SubElement(loop_element, "coordinate", X=str(x), Y=str(y)) 106 | 107 | # Write the updated XML to the output file 108 | try: 109 | tree.write(output_file, encoding="utf-8", xml_declaration=True) 110 | print(f"Processed field loops saved to {output_file}") 111 | except Exception as e: 112 | print(f"Failed to write output XML file: {e}") 113 | raise 114 | 115 | return output_file 116 | -------------------------------------------------------------------------------- /scripts/simplifyFieldLoops.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from shapely.geometry import LineString, Polygon 3 | import os 4 | 5 | class SimplifyFieldLoops: 6 | def process(self, input_file, output_dir, simplification_strength, shrink_distance): 7 | """ 8 | Simplify all loops and shrink loop ID=1 in an XML file. 9 | 10 | Args: 11 | input_file (str): Path to the input XML file. 12 | output_dir (str): Directory to save the simplified XML file. 13 | simplification_strength (float): Simplification tolerance. 14 | shrink_distance (float): Distance to shrink loop ID=1. 15 | 16 | Returns: 17 | str: Path to the saved simplified XML file. 18 | """ 19 | def simplify_coordinates(coordinates, tolerance): 20 | """Simplify coordinates using the Ramer-Douglas-Peucker algorithm.""" 21 | line = LineString(coordinates) 22 | simplified_line = line.simplify(tolerance, preserve_topology=True) 23 | return list(simplified_line.coords) 24 | 25 | def shrink_loop(coordinates, distance): 26 | """ 27 | Shrink or expand a loop contour by a specified distance. 28 | Args: 29 | coordinates (list of tuples): List of (x, y) tuples representing the polygon's vertices. 30 | distance (float): Distance to shrink or expand (negative for inward shrinking). 31 | Returns: 32 | list of tuples: Modified coordinates. 33 | """ 34 | polygon = Polygon(coordinates) 35 | if not polygon.is_valid: 36 | polygon = polygon.buffer(0) # Attempt to fix invalid polygons 37 | 38 | shrunk_polygon = polygon.buffer(-distance) # Negative for shrinking, positive for expanding 39 | 40 | if shrunk_polygon.is_empty: 41 | print("Shrinking resulted in an empty polygon. Returning original coordinates.") 42 | return coordinates # Return original if shrinking collapses the polygon 43 | 44 | if shrunk_polygon.geom_type == "MultiPolygon": 45 | print("Shrinking resulted in multiple polygons. Selecting the largest polygon.") 46 | shrunk_polygon = max(shrunk_polygon, key=lambda p: p.area) # Select the largest polygon 47 | 48 | if shrunk_polygon.geom_type == "Polygon": 49 | return list(shrunk_polygon.exterior.coords) 50 | 51 | print(f"Unexpected geometry type after shrinking: {shrunk_polygon.geom_type}") 52 | return coordinates 53 | 54 | 55 | def process_loop(loop_element, tolerance, shrink_distance): 56 | """Process, simplify, and optionally shrink coordinates within a Loop XML element.""" 57 | original_coords = [] 58 | for coord in loop_element.findall('coordinate'): 59 | x = float(coord.get('X')) 60 | y = float(coord.get('Y')) 61 | original_coords.append((x, y)) 62 | 63 | # Simplify the loop 64 | simplified_coords = simplify_coordinates(original_coords, tolerance) 65 | 66 | # If the loop ID is 1, perform shrinking 67 | loop_id = loop_element.get('ID') 68 | if loop_id == "1" and shrink_distance is not None: 69 | modified_coords = shrink_loop(simplified_coords, shrink_distance) 70 | else: 71 | modified_coords = simplified_coords 72 | 73 | removed = len(original_coords) - len(modified_coords) 74 | 75 | # Clear the original coordinates and replace them with the modified ones 76 | for coord in list(loop_element): 77 | loop_element.remove(coord) 78 | 79 | for x, y in modified_coords: 80 | ET.SubElement(loop_element, 'coordinate', X=str(x), Y=str(y)) 81 | 82 | return removed 83 | 84 | # Define output file path 85 | output_file = os.path.join(output_dir, "simplified_field_loops.xml") 86 | 87 | # Load and process the XML file 88 | print(f"Loading XML file: {input_file}") 89 | tree = ET.parse(input_file) 90 | root = tree.getroot() 91 | 92 | total_original_coords = 0 93 | total_removed_coords = 0 94 | 95 | for field in root.findall('.//Field'): 96 | for loop in field.findall('.//Loop'): 97 | original_count = len(loop.findall('coordinate')) 98 | removed = process_loop(loop, simplification_strength, shrink_distance if loop.get('ID') == "1" else None) 99 | total_original_coords += original_count 100 | total_removed_coords += removed 101 | print(f"Processed loop ID={loop.get('ID')}: {original_count} points, reduced by {removed} points.") 102 | 103 | reduction_percentage = (total_removed_coords / total_original_coords) * 100 if total_original_coords else 0 104 | print(f"Total original points: {total_original_coords}, points removed: {total_removed_coords}, " 105 | f"reduction: {reduction_percentage:.2f}%") 106 | 107 | print(f"Writing simplified XML to: {output_file}") 108 | tree.write(output_file, encoding="utf-8", xml_declaration=True) 109 | 110 | return output_file 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FS25_ImageToFields 2 | 3 | > ## Download version 0.1.0 here: [https://github.com/PixelFarm1/FS25_ImageToFields/releases/tag/v0.1.0-experimental] 4 | 5 | ## German translation below ![image](https://github.com/user-attachments/assets/15acf8fb-474c-4326-a28c-885c138b1e4a) 6 | 7 | 8 | #### For any code wizards looking at this... I'm sorry. I have almost no programming experience and a lot of the code is the work of me with the help of chatGPT. The code is most definately not perfectly set up 9 | 10 | FS25_ImageToFields is a tool for easy creation of field dimensions for FS25. It takes a white on black field mask as input and creates coordinates based on the image. Through some processing it verifies that the coordinates are ordered in a way that allows for complex field shapes. The final processed coordinates are run through the xmlToFields.lua which creates fields and their respective polygons. The GE script also aligns the polygonpoints to the terrain and repaints all fields. All you have to do at the end is run the repaint farmalnds function in the fieldToolkit of GE. 11 | 12 | ![image](https://github.com/user-attachments/assets/cb449c51-b168-4172-9053-d082ce425be3) 13 | 14 | ## This is what a proper field mask looks like 15 | ![image](https://github.com/user-attachments/assets/072c551c-b220-487e-8f28-8bebe1ef1e2a) 16 | 17 | 18 | ## How to use 19 | 1. Make sure that you have a clean field mask. There can be no mistakes in it or you will get a bad result or errors from the program. Common mistakes are: stray white pixels in non-field areas or black pixels in white areas. Too little space between 2 field boundarys (drive an imaginary 1x1 pixel size tractor along all field borders of the mask, if you cannot pass -> fix that area). 20 | 21 | 2. Run the .exe from the latest release (or main.py if you want more work) 22 | 23 | 3. Click "Browse" and choose your field mask. 24 | 25 | 4. Make sure to set the correct DEM size. This is the resolution of your DEM.png in the data folder of your map (-1 pixel). 26 | 27 | 5. It is recommended to not change the settings on the first run but to go with the defaults. Change the settings and run again if you want to make any tweaks to the output. 28 | 29 | 6. Press "Run" to start the processing. The log will tell you the output directory. 30 | 31 | 7. After the processing is finished, you can press "Visualize fields" to show the final output. Toggle the IDs on and off by pressing "Toggle Field IDs". 32 | 33 | 8. Go into Giants Editor and make sure that you have a "Fields" group with the correct attributes. Also remove any childs of the Fields transform group. 34 | 35 | 9. Create a new script in GE and paste the xmlToFields.lua contents to the file. Or just drop the whole .lua in your scripts folder for GE. 36 | 37 | 10. Change the filepath at the bottom of the .lua file. It should point to the location of your final_field_coordinates.xml 38 | 39 | 11. Execute the script. This will clear all existing painted field ground, generate the fields from coordinates, align them to the terrain and then repaint the fields. 40 | 41 | ## Suggested workflow for converting FS22 maps 42 | ### Prerequisites: A FS22 map where all fields are painted with terrainDetail (May work if you take the densityMap_ground.gdm from a FS22 savegame too, not tested) 43 | 44 | 1. Convert the densityMap_ground.gdm using the converter at GDN 45 | 46 | 2. Open the converted file in GIMP and att a new layer with white fill 47 | 48 | 3. If the image turns all red and not white. Press Image -> Mode -> RGB to change to RGB mode. Then recreate the layer with white fill. 49 | 50 | 4. Set the blending mode to "Dodge" and merge the 2 layers 51 | 52 | 5. Press Select -> By color (Shift + O) and press one of the now bright red areas. 53 | 54 | 6. Look for any mistakes like missed pixels, stray pixels etc. They are more easy to spot when in the select mode. 55 | 56 | 7. When you are done correcting the image. Create a new layer and with white fill. 57 | 58 | 8. Change blending mode to "HSV Saturation" and your field areas should turn white. 59 | 60 | 9. Merge the layers and repeat step 6 to find any mistakes in the mask. 61 | 62 | 10. Export with these settings: 63 | ![image](https://github.com/user-attachments/assets/b032a1dc-792b-4017-9600-4cf197ea9113) 64 | 65 | 11. Run the FS25_ImageToFields tool according to the instruction above 66 | 67 | 68 | 69 | # Deutsche Übersetzung 70 | #### Für alle Code-Zauberer, die sich das hier ansehen ... Es tut mir leid. Ich habe fast keine Programmiererfahrung, und ein Großteil des Codes entstand durch meine Arbeit mit der Hilfe von ChatGPT. Der Code ist mit Sicherheit nicht perfekt strukturiert. 71 | 72 | FS25_ImageToFields ist ein Tool zur einfachen Erstellung von Feldgeometrien für FS25. Es nimmt eine Schwarz-Weiß-Feldmaske als Eingabe und erstellt basierend auf dem Bild Koordinaten. Durch eine Reihe von Verarbeitungsschritten wird sichergestellt, dass die Koordinaten in einer Weise geordnet sind, die komplexe Feldformen ermöglicht. Die final verarbeiteten Koordinaten werden durch die xmlToFields.lua-Datei verarbeitet, die Felder und deren jeweilige Polygone erstellt. Das GE-Skript richtet außerdem die Polygonpunkte am Gelände aus und übermalt alle Felder. Alles, was am Ende noch zu tun ist, ist die Funktion "Repaint Farmlands" im FieldToolkit von GE auszuführen. 73 | 74 | ![image](https://github.com/user-attachments/assets/cb449c51-b168-4172-9053-d082ce425be3) 75 | 76 | ## So sieht eine korrekte Feldmaske aus. 77 | ![image](https://github.com/user-attachments/assets/072c551c-b220-487e-8f28-8bebe1ef1e2a) 78 | 79 | ## Anleitung zur Verwendung 80 | 1. Stellen Sie sicher, dass Sie eine saubere Feldmaske haben. 81 | Es dürfen keine Fehler in der Maske vorhanden sein, da dies zu einem schlechten Ergebnis führen kann. Häufige Fehler sind: 82 | 83 | Vereinzelte weiße Pixel in Bereichen, die keine Felder sind. 84 | Schwarze Pixel in weißen Feldbereichen. 85 | 86 | 2. Starten Sie die .exe aus der neuesten Version (oder main.py, falls Sie mehr Arbeit investieren möchten). 87 | 88 | 3. Klicken Sie auf "Browse" und wählen Sie Ihre Feldmaske aus. 89 | 90 | 4.Stellen Sie sicher, dass Sie die korrekte DEM-Größe einstellen. Dies ist die Auflösung Ihrer DEM.png im Datenordner Ihrer Karte minus 1 Pixel. 91 | 92 | 5. Ändern Sie beim ersten Durchlauf die Standardeinstellungen nicht. Verwenden Sie die Standardwerte und ändern Sie die Einstellungen erst nach dem ersten Durchlauf, wenn Sie Anpassungen am Ergebnis vornehmen möchten. 93 | 94 | 6. Drücken Sie auf "Run", um die Verarbeitung zu starten. Der Fortschritt wird im Protokoll angezeigt, und das Ausgabeverzeichnis wird dort angegeben. 95 | 96 | 7. Nach Abschluss der Verarbeitung können Sie auf "Visualize fields" klicken, um das endgültige Ergebnis anzuzeigen. Mit der Schaltfläche "Toggle Field IDs" können Sie die Feld-IDs ein- oder ausblenden. 97 | 98 | 8. Öffnen Sie den Giants Editor (GE) und stellen Sie sicher, dass Sie eine "Fields"-Gruppe mit den richtigen Attributen haben. Entfernen Sie außerdem alle untergeordneten Objekte der "Fields"-Transformationsgruppe. 99 | 100 | 9. Erstellen Sie ein neues Skript in GE und fügen Sie den Inhalt von xmlToFields.lua in die Datei ein. Alternativ können Sie die .lua-Datei in den Skriptordner von GE legen. 101 | 102 | 10. Passen Sie den Dateipfad am Ende der .lua-Datei an. Der Pfad sollte auf die final_field_coordinates.xml zeigen. 103 | 104 | 11. Führen Sie das Skript aus.Es wird alle vorhandenen gemalten Feldflächen löschen, die Felder aus den Koordinaten generieren, sie an das Gelände anpassen und die Felder neu bemalen. 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /scripts/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "CTk": { 3 | "fg_color": [ 4 | "gray92", 5 | "gray14" 6 | ] 7 | }, 8 | "CTkToplevel": { 9 | "fg_color": [ 10 | "gray92", 11 | "gray14" 12 | ] 13 | }, 14 | "CTkFrame": { 15 | "corner_radius": 6, 16 | "border_width": 0, 17 | "fg_color": [ 18 | "gray86", 19 | "gray17" 20 | ], 21 | "top_fg_color": [ 22 | "gray81", 23 | "gray20" 24 | ], 25 | "border_color": [ 26 | "gray65", 27 | "gray28" 28 | ] 29 | }, 30 | "CTkButton": { 31 | "corner_radius": 6, 32 | "border_width": 0, 33 | "fg_color": [ 34 | "#659927", 35 | "#659927" 36 | ], 37 | "hover_color": [ 38 | "#7cbb32", 39 | "#7cbb32" 40 | ], 41 | "border_color": [ 42 | "#3E454A", 43 | "#949A9F" 44 | ], 45 | "text_color": [ 46 | "gray98", 47 | "#DCE4EE" 48 | ], 49 | "text_color_disabled": [ 50 | "gray78", 51 | "gray68" 52 | ] 53 | }, 54 | "CTkLabel": { 55 | "corner_radius": 0, 56 | "fg_color": "transparent", 57 | "text_color": [ 58 | "gray10", 59 | "#DCE4EE" 60 | ] 61 | }, 62 | "CTkEntry": { 63 | "corner_radius": 6, 64 | "border_width": 2, 65 | "fg_color": [ 66 | "#F9F9FA", 67 | "#343638" 68 | ], 69 | "border_color": [ 70 | "#979DA2", 71 | "#565B5E" 72 | ], 73 | "text_color": [ 74 | "gray10", 75 | "#DCE4EE" 76 | ], 77 | "placeholder_text_color": [ 78 | "#659927", 79 | "#659927" 80 | ] 81 | }, 82 | "CTkCheckBox": { 83 | "corner_radius": 6, 84 | "border_width": 3, 85 | "fg_color": [ 86 | "#2CC985", 87 | "#2FA572" 88 | ], 89 | "border_color": [ 90 | "#3E454A", 91 | "#949A9F" 92 | ], 93 | "hover_color": [ 94 | "#0C955A", 95 | "#106A43" 96 | ], 97 | "checkmark_color": [ 98 | "#DCE4EE", 99 | "gray90" 100 | ], 101 | "text_color": [ 102 | "gray10", 103 | "#DCE4EE" 104 | ], 105 | "text_color_disabled": [ 106 | "gray60", 107 | "gray45" 108 | ] 109 | }, 110 | "CTkSwitch": { 111 | "corner_radius": 1000, 112 | "border_width": 3, 113 | "button_length": 0, 114 | "fg_color": [ 115 | "#939BA2", 116 | "#4A4D50" 117 | ], 118 | "progress_color": [ 119 | "#2CC985", 120 | "#2FA572" 121 | ], 122 | "button_color": [ 123 | "gray36", 124 | "#D5D9DE" 125 | ], 126 | "button_hover_color": [ 127 | "gray20", 128 | "gray100" 129 | ], 130 | "text_color": [ 131 | "gray10", 132 | "#DCE4EE" 133 | ], 134 | "text_color_disabled": [ 135 | "gray60", 136 | "gray45" 137 | ] 138 | }, 139 | "CTkRadioButton": { 140 | "corner_radius": 1000, 141 | "border_width_checked": 6, 142 | "border_width_unchecked": 3, 143 | "fg_color": [ 144 | "#2CC985", 145 | "#2FA572" 146 | ], 147 | "border_color": [ 148 | "#3E454A", 149 | "#949A9F" 150 | ], 151 | "hover_color": [ 152 | "#0C955A", 153 | "#106A43" 154 | ], 155 | "text_color": [ 156 | "gray10", 157 | "#DCE4EE" 158 | ], 159 | "text_color_disabled": [ 160 | "gray60", 161 | "gray45" 162 | ] 163 | }, 164 | "CTkProgressBar": { 165 | "corner_radius": 1000, 166 | "border_width": 0, 167 | "fg_color": [ 168 | "#939BA2", 169 | "#4A4D50" 170 | ], 171 | "progress_color": [ 172 | "#2CC985", 173 | "#2FA572" 174 | ], 175 | "border_color": [ 176 | "gray", 177 | "gray" 178 | ] 179 | }, 180 | "CTkSlider": { 181 | "corner_radius": 1000, 182 | "button_corner_radius": 1000, 183 | "border_width": 6, 184 | "button_length": 0, 185 | "fg_color": [ 186 | "#939BA2", 187 | "#4A4D50" 188 | ], 189 | "progress_color": [ 190 | "gray40", 191 | "#AAB0B5" 192 | ], 193 | "button_color": [ 194 | "#2CC985", 195 | "#2FA572" 196 | ], 197 | "button_hover_color": [ 198 | "#0C955A", 199 | "#106A43" 200 | ] 201 | }, 202 | "CTkOptionMenu": { 203 | "corner_radius": 6, 204 | "fg_color": [ 205 | "#2cbe79", 206 | "#2FA572" 207 | ], 208 | "button_color": [ 209 | "#0C955A", 210 | "#106A43" 211 | ], 212 | "button_hover_color": [ 213 | "#0b6e3d", 214 | "#17472e" 215 | ], 216 | "text_color": [ 217 | "gray98", 218 | "#DCE4EE" 219 | ], 220 | "text_color_disabled": [ 221 | "gray78", 222 | "gray68" 223 | ] 224 | }, 225 | "CTkComboBox": { 226 | "corner_radius": 6, 227 | "border_width": 2, 228 | "fg_color": [ 229 | "#F9F9FA", 230 | "#343638" 231 | ], 232 | "border_color": [ 233 | "#659927", 234 | "#659927" 235 | ], 236 | "button_color": [ 237 | "#659927", 238 | "#659927" 239 | ], 240 | "button_hover_color": [ 241 | "#7cbb32", 242 | "#7cbb32" 243 | ], 244 | "text_color": [ 245 | "gray10", 246 | "#DCE4EE" 247 | ], 248 | "text_color_disabled": [ 249 | "gray50", 250 | "gray45" 251 | ] 252 | }, 253 | "CTkScrollbar": { 254 | "corner_radius": 1000, 255 | "border_spacing": 4, 256 | "fg_color": "transparent", 257 | "button_color": [ 258 | "gray55", 259 | "gray41" 260 | ], 261 | "button_hover_color": [ 262 | "gray40", 263 | "gray53" 264 | ] 265 | }, 266 | "CTkSegmentedButton": { 267 | "corner_radius": 6, 268 | "border_width": 2, 269 | "fg_color": [ 270 | "#979DA2", 271 | "gray29" 272 | ], 273 | "selected_color": [ 274 | "#2CC985", 275 | "#2FA572" 276 | ], 277 | "selected_hover_color": [ 278 | "#0C955A", 279 | "#106A43" 280 | ], 281 | "unselected_color": [ 282 | "#979DA2", 283 | "gray29" 284 | ], 285 | "unselected_hover_color": [ 286 | "gray70", 287 | "gray41" 288 | ], 289 | "text_color": [ 290 | "gray98", 291 | "#DCE4EE" 292 | ], 293 | "text_color_disabled": [ 294 | "gray78", 295 | "gray68" 296 | ] 297 | }, 298 | "CTkTextbox": { 299 | "corner_radius": 6, 300 | "border_width": 0, 301 | "fg_color": [ 302 | "#F9F9FA", 303 | "gray23" 304 | ], 305 | "border_color": [ 306 | "#979DA2", 307 | "#565B5E" 308 | ], 309 | "text_color": [ 310 | "gray10", 311 | "#DCE4EE" 312 | ], 313 | "scrollbar_button_color": [ 314 | "gray55", 315 | "gray41" 316 | ], 317 | "scrollbar_button_hover_color": [ 318 | "gray40", 319 | "gray53" 320 | ] 321 | }, 322 | "CTkScrollableFrame": { 323 | "label_fg_color": [ 324 | "gray78", 325 | "gray23" 326 | ] 327 | }, 328 | "DropdownMenu": { 329 | "fg_color": [ 330 | "gray90", 331 | "gray20" 332 | ], 333 | "hover_color": [ 334 | "gray75", 335 | "gray28" 336 | ], 337 | "text_color": [ 338 | "gray10", 339 | "gray90" 340 | ] 341 | }, 342 | "CTkFont": { 343 | "macOS": { 344 | "family": "SF Display", 345 | "size": 13, 346 | "weight": "normal" 347 | }, 348 | "Windows": { 349 | "family": "Roboto", 350 | "size": 13, 351 | "weight": "normal" 352 | }, 353 | "Linux": { 354 | "family": "Roboto", 355 | "size": 13, 356 | "weight": "normal" 357 | } 358 | } 359 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from scripts.gui import App 2 | from scripts.imageConverter import imageConvert 3 | from scripts.imageToCoordinates import createCoordinates 4 | from scripts.processFieldLoops import ProcessFieldLoops 5 | from scripts.simplifyFieldLoops import SimplifyFieldLoops 6 | from scripts.markFieldLoops import MarkFieldLoops 7 | from scripts.finalizeFieldCoordinates import FinalizeFieldCoordinates 8 | import xml.etree.ElementTree as ET 9 | import matplotlib.pyplot as plt 10 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk 11 | from tkinter import TclError 12 | import sys 13 | import os 14 | 15 | def ensure_output_folder_exists(): 16 | """ 17 | Ensures the 'output' folder exists next to the main script. 18 | If the folder does not exist, it is created. 19 | """ 20 | # Get the directory of the current script 21 | #script_dir = os.path.dirname(os.path.abspath(__file__)) 22 | script_dir = get_executable_dir() 23 | 24 | # Path to the 'output' folder next to the script 25 | output_folder = os.path.join(script_dir, "output") 26 | 27 | # Create the folder if it doesn't exist 28 | if not os.path.exists(output_folder): 29 | os.makedirs(output_folder) 30 | print(f"'output' folder created at: {output_folder}") 31 | else: 32 | print(f"'output' folder already exists at: {output_folder}") 33 | 34 | return output_folder 35 | 36 | def get_executable_dir(): 37 | """ 38 | Returns the directory where the executable (or script) is located. 39 | """ 40 | if getattr(sys, 'frozen', False): # Check if running as a PyInstaller executable 41 | return os.path.dirname(sys.executable) 42 | return os.path.dirname(os.path.abspath(__file__)) # If running as a script 43 | 44 | def run_pipeline(input_file, simplificationStrength, distanceThreshold, borderReduction, demSize): 45 | print("Starting the tool...") 46 | 47 | # Ensure the 'output' folder exists 48 | output_folder = ensure_output_folder_exists() 49 | 50 | imageConverter = imageConvert() 51 | converted_image = imageConverter.process(input_file, output_folder) 52 | 53 | createCoordinates1 = createCoordinates() 54 | xml_coordinates = createCoordinates1.process(converted_image, output_folder, demSize) 55 | 56 | processFieldLoops = ProcessFieldLoops() 57 | processedLoops_xml = processFieldLoops.process(xml_coordinates, output_folder, distanceThreshold) 58 | 59 | simplifyFieldLoops = SimplifyFieldLoops() 60 | simplified_xml = simplifyFieldLoops.process(processedLoops_xml, output_folder, simplificationStrength, borderReduction) 61 | 62 | markFieldLoops = MarkFieldLoops() 63 | marked_xml = markFieldLoops.process(simplified_xml, output_folder) 64 | 65 | finalizeFieldCoordinates = FinalizeFieldCoordinates() 66 | final_output = finalizeFieldCoordinates.process(marked_xml, output_folder) 67 | 68 | return final_output # Return the final output file path 69 | 70 | class TextRedirector: 71 | def __init__(self, text_widget): 72 | self.text_widget = text_widget 73 | 74 | def write(self, message): 75 | self.text_widget.configure(state="normal") 76 | self.text_widget.insert("end", message) 77 | self.text_widget.see("end") 78 | self.text_widget.configure(state="disabled") 79 | 80 | def flush(self): 81 | pass 82 | 83 | class MyApp(App): 84 | def __init__(self): 85 | super().__init__() 86 | 87 | self.run_button.configure(command=self.start_pipeline_thread) 88 | self.viz_button.configure(command=self.visualize_fields) 89 | self.toggle_button.configure( 90 | command=self.toggle_labels, 91 | state="disabled" # Initially disabled 92 | ) 93 | 94 | self.after_tasks = [] # Track all after tasks 95 | 96 | # Save original stdout and stderr 97 | self.original_stdout = sys.stdout 98 | self.original_stderr = sys.stderr 99 | 100 | # Redirect stdout and stderr to the GUI 101 | sys.stdout = TextRedirector(self.log_box) 102 | sys.stderr = TextRedirector(self.log_box) 103 | 104 | 105 | # Initialize variables 106 | self.text_labels = [] 107 | self.canvas = None 108 | 109 | # Handle window closing 110 | self.protocol("WM_DELETE_WINDOW", self.on_closing) 111 | 112 | def after_task(self, delay, func, *args): 113 | """ 114 | Schedule a task with `after` and track it for cleanup. 115 | """ 116 | task_id = self.after(delay, func, *args) 117 | self.after_tasks.append(task_id) 118 | return task_id 119 | 120 | def cancel_after_tasks(self): 121 | """ 122 | Cancel all pending `after` tasks. 123 | """ 124 | for task_id in self.after_tasks: 125 | try: 126 | self.after_cancel(task_id) 127 | except Exception: 128 | pass 129 | self.after_tasks.clear() 130 | 131 | def on_closing(self): 132 | """ 133 | Clean up and close the program properly. 134 | """ 135 | try: 136 | # Cancel all scheduled tasks 137 | self.cancel_after_tasks() 138 | 139 | # Disable CustomTkinter scaling to prevent DPI errors 140 | try: 141 | from customtkinter.windows.widgets.scaling.scaling_tracker import ScalingTracker 142 | ScalingTracker.get_instance().reset_scaling() 143 | except ImportError: 144 | pass # ScalingTracker may not exist in all versions of CustomTkinter 145 | 146 | # Restore stdout and stderr 147 | sys.stdout = self.original_stdout 148 | sys.stderr = self.original_stderr 149 | 150 | # Destroy the GUI safely 151 | self.destroy() 152 | except TclError as e: 153 | print(f"Handled TclError: {e}", file=self.original_stderr) 154 | finally: 155 | # Ensure program exits properly 156 | sys.exit(0) 157 | 158 | def start_pipeline_thread(self): 159 | import threading 160 | input_file = self.file_input.get() 161 | simplificationStrength = float(self.slider1.get()) 162 | distanceThreshold = int(self.slider2.get()) 163 | borderReduction = int(self.slider3.get()) 164 | demSize = int(self.demSize.get()) 165 | threading.Thread( 166 | target=self.run_pipeline_safe, 167 | args=(input_file, simplificationStrength, distanceThreshold, borderReduction, demSize), 168 | ).start() 169 | 170 | def run_pipeline_safe(self, input_file, simplificationStrength, distanceThreshold, borderReduction, demSize): 171 | try: 172 | print("Running tool...") 173 | final_output = run_pipeline(input_file, simplificationStrength, distanceThreshold, borderReduction, demSize) 174 | except Exception as e: 175 | self.log_message(f"Error: {e}") 176 | 177 | 178 | 179 | def visualize_fields(self): 180 | """ 181 | Searches the output folder for final_field_coordinates.xml and visualizes the fields. 182 | If the file is not found, prompts the user to run the pipeline first. 183 | """ 184 | # Determine the output folder location 185 | output_folder = ensure_output_folder_exists() 186 | final_output_file = os.path.join(output_folder, "final_field_coordinates.xml") 187 | 188 | if not os.path.exists(final_output_file): 189 | self.log_message(f"Error: {final_output_file} not found. Please run the tool first.") 190 | return 191 | 192 | try: 193 | self.log_message("Visualizing fields...") 194 | self.display_plot_in_gui(final_output_file) 195 | self.log_message("Visualization completed successfully.") 196 | self.toggle_button.configure(state="normal") # Enable the toggle button 197 | except Exception as e: 198 | self.log_message(f"Visualization error: {e}") 199 | 200 | def display_plot_in_gui(self, file_path): 201 | """ 202 | Renders the visualization directly in the GUI with zoom/pan functionality and toggle for field IDs. 203 | """ 204 | fields, centers = parse_all_field_coordinates_with_center_flipped_plane(file_path) 205 | fig, ax = plt.subplots(figsize=(6, 6)) 206 | 207 | self.text_labels = [] # Reset text_labels for each plot 208 | 209 | for field_id, coords in fields.items(): 210 | x, y = zip(*coords) 211 | ax.plot(x, y, label=f"Field {field_id}", color="black") 212 | center_x, center_y = centers[field_id] 213 | text = ax.text( 214 | center_x, center_y, f"ID {field_id}", fontsize=9, ha='center', va='center', 215 | bbox=dict(boxstyle="round,pad=0.3", edgecolor="black", facecolor="lightyellow", alpha=0.8) 216 | ) 217 | self.text_labels.append(text) # Store the text element 218 | 219 | ax.set_xlabel("X Coordinate") 220 | ax.set_ylabel("Z Coordinate") 221 | ax.set_title("Visualization of All Fields") 222 | ax.grid(True) 223 | 224 | # Clear the plot_frame 225 | for widget in self.plot_frame.winfo_children(): 226 | widget.destroy() 227 | 228 | # Embed the Matplotlib figure into the Tkinter frame 229 | self.canvas = FigureCanvasTkAgg(fig, master=self.plot_frame) 230 | self.canvas.draw() 231 | self.canvas.get_tk_widget().pack(fill="both", expand=True) 232 | 233 | # Add a Matplotlib navigation toolbar 234 | toolbar = NavigationToolbar2Tk(self.canvas, self.plot_frame) 235 | toolbar.update() 236 | toolbar.pack(side="bottom", fill="x") 237 | 238 | # Enable the toggle button after plot creation 239 | self.toggle_button.configure(state="normal") 240 | 241 | # Add a toggle button for field ID labels 242 | def toggle_labels(self): 243 | """ 244 | Toggles the visibility of field ID labels. 245 | """ 246 | if not self.text_labels: 247 | return # Do nothing if there are no labels 248 | 249 | # Determine the current visibility of the first label and toggle all labels 250 | visible = not self.text_labels[0].get_visible() 251 | for label in self.text_labels: 252 | label.set_visible(visible) 253 | 254 | # Redraw the canvas to apply visibility changes 255 | if self.canvas: 256 | self.canvas.draw_idle() 257 | 258 | 259 | 260 | def parse_all_field_coordinates_with_center_flipped_plane(file_path): 261 | tree = ET.parse(file_path) 262 | root = tree.getroot() 263 | 264 | fields = {} 265 | centers = {} 266 | for field in root.findall("Field"): 267 | field_id = field.attrib.get("ID") 268 | center_x = float(field.attrib.get("X")) 269 | center_y = -float(field.attrib.get("Y")) # Flip the Y-plane for the center 270 | 271 | coordinates = [ 272 | (center_x + float(coord.attrib["X"]), center_y - float(coord.attrib["Y"])) # Flip Y-plane for center and coords 273 | for coord in field.findall("coordinate") 274 | ] 275 | fields[field_id] = coordinates 276 | centers[field_id] = (center_x, center_y) 277 | return fields, centers 278 | 279 | 280 | if __name__ == "__main__": 281 | app = MyApp() 282 | app.mainloop() 283 | ensure_output_folder_exists() 284 | -------------------------------------------------------------------------------- /scripts/gui.py: -------------------------------------------------------------------------------- 1 | import customtkinter as ctk 2 | import tkinter.filedialog as fd 3 | import os 4 | 5 | class App(ctk.CTk): 6 | def __init__(self): 7 | super().__init__() 8 | self.title("FS25 Image to fields") 9 | self.geometry("1800x800") 10 | ctk.set_appearance_mode("dark") 11 | theme_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "theme.json") 12 | if not os.path.exists(theme_path): 13 | print(f"Error: Theme file not found at {theme_path}") 14 | ctk.set_default_color_theme(theme_path) 15 | 16 | self.info_box = None 17 | self.hover_job = None 18 | 19 | self.log_frame = ctk.CTkFrame(self) 20 | self.log_frame.grid(row=0, column=0, sticky="nsew", pady=10, padx=5) 21 | self.log_frame.grid_columnconfigure(0, weight=1) 22 | self.log_frame.grid_rowconfigure(2, weight=1) 23 | self.log_frame.grid_rowconfigure(1, weight=1) 24 | 25 | self.title_label = ctk.CTkLabel(self.log_frame, text="Information", fg_color="#659927", corner_radius=5, height=30, font=("Roboto",15,"bold")) 26 | self.title_label.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="new") 27 | self.log_box = ctk.CTkTextbox(self.log_frame, width=600, height=750, state="normal", wrap="word") 28 | self.log_box.grid(row=1, column=0, padx=5, pady=10, sticky="nsew") 29 | self.log_box.configure(state="disabled") # Initially read-only 30 | 31 | # Input frame 32 | self.input_frame = ctk.CTkFrame(self) 33 | self.input_frame.grid(row=0, column=1, sticky="nsew", pady=10, padx=5) 34 | self.input_frame.grid_rowconfigure(2, weight=1) 35 | self.input_frame.grid_columnconfigure(0, weight=1) 36 | self.input_frame.grid_rowconfigure(1, weight=1) # Ensure var_frame can expand 37 | 38 | self.files_frame = ctk.CTkFrame(self.input_frame) 39 | self.files_frame.grid(row=0, padx=10, pady=10, sticky="nsew") 40 | self.files_frame.grid_columnconfigure(0, weight=1) 41 | 42 | # File input 43 | self.file_input = ctk.CTkEntry(self.files_frame, placeholder_text="Select PNG file for processing") 44 | self.file_input.grid(pady=5, padx=5, sticky="ew") 45 | self.browse_button = ctk.CTkButton(self.files_frame, text="Browse", font=("", 15, "bold"), command=self.browse_file) 46 | self.browse_button.grid(pady=5, padx=5, sticky="ew") 47 | 48 | self.var_frame = ctk.CTkFrame(self.input_frame) 49 | self.var_frame.grid(row=1, padx=10, pady=10, sticky="nsew") 50 | self.var_frame.grid_rowconfigure(0, weight=1) # For dem_frame 51 | self.var_frame.grid_rowconfigure(1, weight=1) # For slider_frame 52 | self.var_frame.grid_columnconfigure(0, weight=1) 53 | 54 | self.dem_frame = ctk.CTkFrame(self.var_frame) 55 | self.dem_frame.grid(row=0, padx=10, pady=10, sticky="nsew") 56 | self.dem_frame.grid_columnconfigure(0, weight=1) 57 | 58 | #DEM size 59 | self.demSize_label = ctk.CTkLabel(self.dem_frame, text="Select the size of your DEM") 60 | self.demSize_label.grid(pady=5, padx=5, sticky="ew") 61 | self.demSize = ctk.CTkComboBox(self.dem_frame, values=["1024", "2048", "4096", "8192"]) 62 | self.demSize.set("2048") 63 | self.demSize.grid(pady=5, padx=5, sticky="ew") 64 | 65 | 66 | # Add sliders for variables 67 | self.slider_frame = ctk.CTkFrame(self.var_frame) 68 | self.slider_frame.grid(row=1, padx=10, pady=10, sticky="nsew") 69 | self.slider_frame.grid_columnconfigure(0, weight=1) 70 | 71 | self.slider1_label = ctk.CTkLabel(self.slider_frame, text="Simplification Strength") 72 | self.slider1_label.grid(sticky="ew", pady=5, padx=5) 73 | 74 | self.slider1 = ctk.CTkSlider(self.slider_frame, from_=0, to=1, number_of_steps=10, 75 | command=self.update_simplification_strength, 76 | progress_color="#7cbb32", 77 | button_color="#7cbb32", 78 | button_hover_color="#7cbb32") 79 | self.slider1.set(0.2) 80 | self.slider1.grid(sticky="ew", pady=5, padx=5) 81 | 82 | self.slider1_dis = ctk.CTkEntry(self.slider_frame) 83 | self.slider1_dis.grid(sticky="ew", pady=5, padx=5) 84 | self.slider1_dis.insert(0, str(0.2)) 85 | 86 | self.slider1.bind("", lambda e: self.slider1_dis.delete(0, "end") or self.slider1_dis.insert(0, round(self.slider1.get(),1))) 87 | self.slider1_dis.bind("", lambda e: self.slider1.set(float(self.slider1_dis.get()))) 88 | 89 | self.slider2_label = ctk.CTkLabel(self.slider_frame, text="Distance Threshold") 90 | self.slider2_label.grid(sticky="ew", pady=5, padx=5) 91 | 92 | self.slider2 = ctk.CTkSlider(self.slider_frame, from_=0, to=20, number_of_steps=20, 93 | command=self.update_distance_threshold, 94 | progress_color="#7cbb32", 95 | button_color="#7cbb32", 96 | button_hover_color="#7cbb32") 97 | self.slider2.set(10) 98 | self.slider2.grid(sticky="ew", pady=5, padx=5) 99 | 100 | self.slider2_dis = ctk.CTkEntry(self.slider_frame) 101 | self.slider2_dis.grid(sticky="ew", pady=5, padx=5) 102 | self.slider2_dis.insert(0, 10) 103 | 104 | self.slider2.bind("", lambda e: self.slider2_dis.delete(0, "end") or self.slider2_dis.insert(0, int(self.slider2.get()))) 105 | self.slider2_dis.bind("", lambda e: self.slider2.set(int(self.slider2_dis.get()))) 106 | 107 | self.slider3_label = ctk.CTkLabel(self.slider_frame, text="Border reduction") 108 | self.slider3_label.grid(sticky="ew", pady=5, padx=5) 109 | 110 | self.slider3 = ctk.CTkSlider(self.slider_frame, from_=0, to=10, number_of_steps=10, 111 | command=self.update_shrink_amount, 112 | progress_color="#7cbb32", 113 | button_color="#7cbb32", 114 | button_hover_color="#7cbb32") 115 | self.slider3.set(0) 116 | self.slider3.grid(sticky="ew", pady=5, padx=5) 117 | 118 | self.slider3_dis = ctk.CTkEntry(self.slider_frame) 119 | self.slider3_dis.grid(sticky="ew", pady=5, padx=5) 120 | self.slider3_dis.insert(0, 0) 121 | 122 | self.slider3.bind("", lambda e: self.slider3_dis.delete(0, "end") or self.slider3_dis.insert(0, int(self.slider3.get()))) 123 | self.slider3_dis.bind("", lambda e: self.slider3.set(int(self.slider3_dis.get()))) 124 | 125 | # Run frame 126 | self.run_frame = ctk.CTkFrame(self.input_frame) 127 | self.run_frame.grid(row=2, column=0, padx=10, pady=10, sticky="new") 128 | self.run_frame.grid_columnconfigure(0, weight=1) 129 | 130 | # Run button 131 | self.run_button = ctk.CTkButton(self.run_frame, text="Run", height=50, font=("", 20)) 132 | self.run_button.grid(column=0, row=0, pady=5, padx=5, sticky="ew") 133 | 134 | # Visualize button 135 | self.viz_button = ctk.CTkButton(self.run_frame, text="Visualize fields", height=30, font=("", 20)) 136 | self.viz_button.grid(column=0, row=1, pady=5, padx=5, sticky="ew") 137 | 138 | # Toggle Field IDs button 139 | self.toggle_button = ctk.CTkButton(self.run_frame, text="Toggle Field IDs", height=30, font=("", 15)) 140 | self.toggle_button.grid(column=0, row=2, pady=5, padx=5, sticky="ew") 141 | 142 | 143 | # Add a frame for the plot 144 | self.plot_frame = ctk.CTkFrame(self, height=600, width=600) 145 | self.plot_frame.grid(row=0, column=2, sticky="nsew", padx=10, pady=10) 146 | self.plot_frame.grid_propagate(False) # Prevent resizing based on content 147 | 148 | 149 | # Configure grid weights 150 | self.grid_rowconfigure(0, weight=1) 151 | self.grid_columnconfigure(0, weight=2) 152 | self.grid_columnconfigure(1, weight=1) 153 | self.grid_columnconfigure(2, weight=3) 154 | 155 | self.slider1_label_tooltip = "Controls the level of simplification applied to the geometry." 156 | self.slider2_label_tooltip = "Sets the maximum allowable distance between points to class it as an 'island'." 157 | self.slider3_label_tooltip = "Units of reduction of the field outline. Only affects the outer loop." 158 | self.demSize_label_tooltip = "If DEM is size 2049x2049. Choose 2048." 159 | self.bind_tooltip("slider1_label") 160 | self.bind_tooltip("slider2_label") 161 | self.bind_tooltip("slider3_label") 162 | self.bind_tooltip("demSize_label") 163 | 164 | 165 | def update_simplification_strength(self, value): 166 | self.simplification_strength = round(float(value), 2) 167 | 168 | def update_distance_threshold(self, value): 169 | self.distance_threshold = int(value) 170 | 171 | def update_shrink_amount(self, value): 172 | self.shrink_amount = int(value) 173 | 174 | #Information boxes 175 | def show_info(self, event, text): 176 | # Schedule the info box to appear after a delay (e.g., 500ms) 177 | self.hover_job = self.after(200, lambda: self._create_info_box(event, text)) 178 | 179 | def _create_info_box(self, event, text): 180 | # Ensure only one info box is visible 181 | if self.info_box is not None: 182 | self.info_box.destroy() 183 | 184 | # Create a new info box 185 | self.info_box = ctk.CTkToplevel(self) 186 | self.info_box.wm_overrideredirect(True) 187 | self.info_box.geometry(f"+{event.x_root + 10}+{event.y_root + 10}") 188 | 189 | # Add content to the info box 190 | info_label = ctk.CTkLabel(self.info_box, text=text, padx=10, pady=5, corner_radius=5) 191 | info_label.pack() 192 | 193 | def hide_info(self, event): 194 | # Cancel the scheduled job if it exists 195 | if self.hover_job is not None: 196 | self.after_cancel(self.hover_job) 197 | self.hover_job = None 198 | 199 | # Destroy the current info box 200 | if self.info_box is not None: 201 | self.info_box.destroy() 202 | self.info_box = None 203 | 204 | def bind_tooltip(self, widget_name): 205 | # Dynamically construct the tooltip variable name 206 | tooltip_name = f"{widget_name}_tooltip" 207 | 208 | # Check if the tooltip variable exists 209 | tooltip_text = getattr(self, tooltip_name, None) 210 | 211 | if tooltip_text is not None: 212 | widget = getattr(self, widget_name) 213 | widget.bind("", lambda e: self.show_info(e, tooltip_text)) 214 | widget.bind("", self.hide_info) 215 | 216 | def browse_file(self): 217 | # Open a file dialog to select a file 218 | file_path = fd.askopenfilename( 219 | title="Select a PNG File", 220 | filetypes=(("PNG files", "*.png"), ("All files", "*.*")) 221 | ) 222 | 223 | # If a file is selected, set its path in the file_input entry 224 | if file_path: 225 | self.file_input.delete(0, "end") # Clear the entry 226 | self.file_input.insert(0, file_path) # Insert the selected file path 227 | 228 | def log_message(self, message): 229 | self.log_box.configure(state="normal") # Enable editing 230 | self.log_box.insert("end", f"{message}\n") # Insert the message 231 | self.log_box.see("end") # Auto-scroll to the end 232 | self.log_box.configure(state="disabled") # Disable editing 233 | 234 | if __name__ == "__main__": 235 | app = App() 236 | app.mainloop() --------------------------------------------------------------------------------