From b0a37e1ae76c2ea43629efa046b1bd6213d8b68b Mon Sep 17 00:00:00 2001 From: Falko Habel Date: Sun, 10 Mar 2024 16:39:21 +0100 Subject: [PATCH 1/2] Folder Mangement has been added --- scripts/folder_mangement/CreateFolder.py | 93 ++++++++++++++++++ scripts/folder_mangement/OpenFolder.py | 99 +++++++++++++++++++ scripts/folder_mangement/SwitchFolder.py | 117 +++++++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 scripts/folder_mangement/CreateFolder.py create mode 100644 scripts/folder_mangement/OpenFolder.py create mode 100644 scripts/folder_mangement/SwitchFolder.py diff --git a/scripts/folder_mangement/CreateFolder.py b/scripts/folder_mangement/CreateFolder.py new file mode 100644 index 0000000..3f13bcf --- /dev/null +++ b/scripts/folder_mangement/CreateFolder.py @@ -0,0 +1,93 @@ +import customtkinter as Ctk +import os +import scripts.get_sys_info as system_code +from ..SaveData import SaveData + +FONT = "Berlin Sans FB" + +class CreateFolder(Ctk.CTkFrame): + def __init__(self,master, output_path,callback, **kwargs): + super().__init__(master, **kwargs) + system_code.load_json_file() + self.data_saver = SaveData() + self.my_font = Ctk.CTkFont(family=FONT, size=22) + self.output_path = output_path + self.callback = callback + self.error_txt = "" + + self.create_folder() + + def enable_keybinding(self): + self.master.bind("", self.on_a_press, add="+") + + def disable_keybinding(self): + self.master.unbind("") + + def on_a_press(self, event): + # Check if the frame is visible by querying its manager info + if self.winfo_manager(): + self.create_new_data_folder() + + def create_folder(self): + self.enable_keybinding() + self.create_folder_label = Ctk.CTkLabel(self, text="Type name for new Object:", width=100, font=self.my_font) + self.create_folder_entry = Ctk.CTkEntry(self, placeholder_text="New Data Object", width=100, font=self.my_font) + self.create_folder_name_btn = Ctk.CTkButton(self, text="Create", width=100, command=self.create_new_data_folder, font=self.my_font) + self.create_folder_error = Ctk.CTkLabel(self, text=self.error_txt, width=100, font=self.my_font) + self.place_create_folder() + + def create_new_data_folder(self): + """ + Create a new folder in the directory specified by self.output_path. + + The folder name will be 'data_folder'. If the folder already exists, a number will be appended + to create a unique folder name ('data_folder_1', 'data_folder_2', etc.). + """ + folder_name = self.create_folder_entry.get() + + # Construct the full path for the new data folder + full_path = os.path.join(self.output_path, folder_name) + if os.path.exists(full_path): + self.error_txt = "The folder exists already" + self.create_folder_error.configure(text=self.error_txt) + return + try: + os.makedirs(full_path) + print(f"Folder created at: {full_path}") + self.data_saver.create(full_path) + except OSError: + self.error_txt = "There occured an error creating the Folder" + self.create_folder_error.configure(text=self.error_txt) + self.callback(full_path) + + + def place_create_folder(self): + self.create_folder_label.place( + relx=0.5, + rely=0.425, + relwidth=0.3, + relheight=0.06, + anchor="center", + ) # Position the converter button to the left + self.create_folder_entry.place( + relx=0.3875, + rely=0.5, + relwidth=0.125, + relheight=0.06, + anchor="w", + ) # Position the converter button to the left + self.create_folder_name_btn.place( + relx=0.6125, + rely=0.5, + relwidth=0.08, + relheight=0.06, + anchor="e", + ) # Adjusted for .place + self.create_folder_error.place( + relx=0.5, + rely=0.575, + relwidth=0.3, + relheight=0.06, + anchor="center", + ) # Position the converter button to the left + diff --git a/scripts/folder_mangement/OpenFolder.py b/scripts/folder_mangement/OpenFolder.py new file mode 100644 index 0000000..d4b902a --- /dev/null +++ b/scripts/folder_mangement/OpenFolder.py @@ -0,0 +1,99 @@ +import customtkinter as Ctk +import os +from PIL import Image +from ..SaveData import SaveData +import scripts.get_sys_info as system_code + + +FONT = "Berlin Sans FB" + +class OpenFolder(Ctk.CTkScrollableFrame): + def __init__(self,master,output_path, callback, **kwargs, ): + super().__init__(master, **kwargs) + self.my_font = Ctk.CTkFont(family=FONT, size=16) + self.warning_font = Ctk.CTkFont(family=FONT, size=20) + # the variables needed to get from the upper class + self.button_image = Ctk.CTkImage(Image.open(r"./icons/folder.png"), size=(63, 63)) + self.output_path = output_path + self.callback = callback # Store the callback function + system_code.load_json_file() + self.data_saver = SaveData() + self.folders = self.get_all_folders() + self.button_size = (50, 50) # Size width and height + self.padding = 10 # Padding around each button + if len(self.folders) == 0: + self.folder_has_no_subs() + else: + self.make_buttons() + self.bind("", self.on_configure, '+') + + def on_configure(self, event=None): + self.rearrange_buttons() + + def get_button_widths(self): + """ + Estimates button widths based on button text size. + """ + average_char_width = 8 + width = max(len(button._text) * average_char_width for button in self.buttons) + return max(width, self.button_size[0]) + + def get_max_folders_per_row(self): + """ + Calculate the maximum number of folders that can fit in a row based on the container's width. + """ + container_width = self.winfo_width() + button_width = self.get_button_widths() + # Calculate the space required for one button including padding + total_button_width = button_width + 2 * self.padding + # Divide the available space in the container by the space required for one button + max_folders_per_row = max(1, container_width // total_button_width) + + return max_folders_per_row + + def get_all_folders(self): + folders = {f.name: f.path for f in os.scandir(self.output_path) if f.is_dir()} + return dict(sorted(folders.items())) + + def rearrange_buttons(self): + """ + Rearrange buttons based on the current window size and calculated folders per row. + If the window size changes, it recalculates the arrangement. + """ + max_folders_per_row = self.get_max_folders_per_row() + + for index, button in enumerate(self.buttons): + row = index // max_folders_per_row + col = index % max_folders_per_row + button.grid(row=row, column=col, padx=self.padding, pady=self.padding) + + def make_buttons(self): + """ + Generates buttons for each folder provided. + """ + self.buttons = [] + for key, value in self.folders.items(): + folder_btn = Ctk.CTkButton( + self, text=key, + width=self.button_size[0], height=self.button_size[1], + image=self.button_image, # Corrected: Use self.button_image for the image parameter + compound="top", # Position text below the image + command=lambda val=value: self.new_folder_chosen(val), + font=self.my_font + ) + folder_btn.grid_remove() + self.buttons.append(folder_btn) + self.rearrange_buttons() # Rearrange buttons after creation + + def new_folder_chosen(self, folder_path): + """ + Callback for when a new folder is chosen. + """ + self.data_saver.search_in_folder(folder_path) + + self.callback(folder_path) # Call the callback passing the current value + + def folder_has_no_subs(self): + self.place_forget() + self.label = Ctk.CTkLabel(self, text="No Folders were found", width=100, font=self.warning_font) + self.label.grid(row = 1, column = 1, padx= 50, pady = 50) \ No newline at end of file diff --git a/scripts/folder_mangement/SwitchFolder.py b/scripts/folder_mangement/SwitchFolder.py new file mode 100644 index 0000000..a38662f --- /dev/null +++ b/scripts/folder_mangement/SwitchFolder.py @@ -0,0 +1,117 @@ +import customtkinter as Ctk +import os +import scripts.get_sys_info as system_code +from ..SaveData import SaveData +from tkinter import filedialog + + +FONT = "Berlin Sans FB" + +class SwitchFolder(Ctk.CTkFrame): + def __init__(self,master, callback, **kwargs): + super().__init__(master, **kwargs) + system_code.load_json_file() + self.data_saver = SaveData() + self.my_font = Ctk.CTkFont(family=FONT, size=22) + self.callback = callback + self.error_txt = "" + self.input_path = None + self.output_path = None + self.img_paths = None + + self.create_selection() + # the selection part + def create_selection(self): + # input + self.input_entry = Ctk.CTkEntry(self, placeholder_text="Input folder path", width=100, font=self.my_font) + self.input_btn = Ctk.CTkButton(self, text="Browse", width=100, command=self.get_folder_path, font=self.my_font) + # output + self.output_entry = Ctk.CTkEntry(self, placeholder_text="Output folder path", width=100, font=self.my_font) + self.output_btn = Ctk.CTkButton(self, text="Browse", width=100, command=self.get_project_path, font=self.my_font) + # start + self.start_btn = Ctk.CTkButton(self, text="Start", width=100, command=self.open_labeling, font=self.my_font) + self.error_label = Ctk.CTkLabel(self, text=self.error_txt, width=100, font=self.my_font) + self.place_label_selection() + + def open_labeling(self): + source_path = self.input_entry.get() + self.output_path = self.output_entry.get() + if not os.path.exists(source_path): + self.error_txt = "Source Folder could not be found" + self.error_label.configure(text=self.error_txt) + elif not os.path.exists(self.output_path): + self.error_txt = "Output Folder could not be found" + self.error_label.configure(text=self.error_txt) + else: + self.data_saver.search_in_folder(self.output_path) + self.img_paths = self.load_images_into_array(source_path) + self.give_back() + + def load_images_into_array(self, folder_path): + image_paths = [] + for filename in os.listdir(folder_path): + if filename.endswith(system_code.img_format_options): + img_path = os.path.join(folder_path, filename) + image_paths.append(img_path) + return image_paths + + def get_folder_path(self): + file_path = filedialog.askdirectory() + self.input_entry.delete(0, Ctk.END) # Delete any existing text in the Entry widget + self.input_entry.insert(0, file_path) # Insert the new text + + def get_project_path(self): + video_path = filedialog.askdirectory() + self.output_entry.delete(0, Ctk.END) # Delete any existing text in the Entry widget + self.output_entry.insert(0, video_path) # Insert the new text + + def give_back(self): + self.callback(self.img_paths, self.output_path) + + def place_label_selection(self): + # output placing + self.input_entry.place( + relx=0.10, + rely=0.4, + anchor="w", + relwidth=0.5, + relheight=0.06 + ) # Position the converter button to the left + self.input_btn.place( + relx=0.90, + rely=0.4, + anchor="e", + relwidth=0.2, + relheight=0.06 + ) # Position the converter button to the left + + # output placing + self.output_entry.place( + relx=0.10, + rely=0.5, + anchor="w", + relwidth=0.5, + relheight=0.06 + ) # Position the converter button to the left + self.output_btn.place( + relx=0.90, + rely=0.5, + anchor="e", + relwidth=0.2, + relheight=0.06 + ) # Position the converter button to the left + # button row + self.start_btn.place( + relx=0.5, + rely=0.6, + anchor="center", + relwidth=0.8, + relheight=0.06 + ) # Position the converter button to the left + self.error_label.place( + relx=0.5, + rely=0.7, + anchor="center", + relwidth=0.8, + relheight=0.06 + ) # Position the converter button to the left \ No newline at end of file From ad83ad1fb248021daba9a38400128102901e991c Mon Sep 17 00:00:00 2001 From: Falko Habel Date: Sun, 10 Mar 2024 16:40:16 +0100 Subject: [PATCH 2/2] Data Mangement Tool added --- scripts/SaveData.py | 157 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 scripts/SaveData.py diff --git a/scripts/SaveData.py b/scripts/SaveData.py new file mode 100644 index 0000000..081981c --- /dev/null +++ b/scripts/SaveData.py @@ -0,0 +1,157 @@ +import os +import glob +import json +import re + + +class SaveData: + def search_in_folder(self, folder, file_name = "MPENN_image_data_information.json"): + """ + Searches for a specific file named "MPENN_image_data_information.json" within a given folder. + + :param folder: The folder path to search in. + :return: A list containing the path to the "MPENN_image_data_information.json" file if found, empty otherwise. + """ + # Validate input parameters + if not os.path.isdir(folder): + raise ValueError("The provided folder path does not exist or is not a directory.") + + # Construct the full search path + search_path = os.path.join(folder, file_name) + + # Use glob.glob to find the specified file + matching_file = glob.glob(search_path) + if len(matching_file) == 0: + matching_file = self.create(folder) + return matching_file + + def create(self, folder): + """ + Create a .json file in the given folder path. + + :param folder: Path of the folder where the .json file will be created. + """ + # Validating folder path + if not isinstance(folder, str) or not folder: + raise ValueError("Folder path must be a non-empty string.") + + # Ensuring the folder exists + os.makedirs(folder, exist_ok=True) + + # Generating a JSON file name with a placeholder approach + file_name = "MPENN_image_data_information.json" + file_path = os.path.join(folder, file_name) + + # Creating and writing to the JSON file + try: + with open(file_path, 'w') as json_file: + json.dump({}, json_file) # Creating an empty JSON object + print(f"File '{file_name}' successfully created at '{folder}'.") + return file_path + except OSError as e: + raise OSError(f"Failed to create file in {folder}. Error: {e}") + + def append_to_json_file(self, image_path, resolution, labeled, rectangles, filename): + """ + Appends a new record to an existing .json file or creates a file if it doesn't exist. + + :param image_path: A string representing the path to the image. + :param resolution: A list or tuple with two integers representing the image's resolution. + :param labeled: A boolean indicating if the image is labeled. + :param rectangles: A list of lists, where each inner list represents rectangle information. + :param filename: A string representing the name of the output .json file. + """ + rectangles = self.calculate_missing_coordinates_for_each(rectangles) + # New entry to add + + + new_entry = { + "Image_path": image_path, + "resolution": resolution, + "labeled": str(labeled), + "rectangle": rectangles + } + + try: + # Try to read the existing file + with open(filename, 'r') as file: + # Load the JSON data from the file + data = json.load(file) + # Ensure the data is a list to hold multiple entries + if not isinstance(data, list): + data = [data] # Convert to list if it's a single dictionary + except FileNotFoundError: + # If the file doesn't exist, start a new list + data = [] + + # Append the new entry + data.append(new_entry) + + # Write the updated list back to the file + with open(filename, 'w') as file: + json.dump(data, file, indent=4) + + print(f"Entry added to {filename}.") + + def calculate_missing_coordinates_for_each(self, rectangles): + """ + Calculate missing coordinates for each rectangle set in + the provided list of rectangles, considering the new format wherein + coordinates are provided as a single tuple. + + Args: + - rectangles (List[List]): Input list containing rectangle identifiers, their coordinates in a single tuple, and other information. + Example: [[id, (top_left_x, top_left_y, bottom_right_x, bottom_right_y), ...]] + + Returns: + - List[List]: Updated list with added missing coordinate lists for each rectangle. + """ + updated_rectangles = [] + + for rectangle in rectangles: + # Assuming the structure is [rectangle_id, (top_left_x, top_left_y, bottom_right_x, bottom_right_y), other_info...] + rectangle_id, coords, *others = rectangle + + # Unpack coords + top_left_x, top_left_y, bottom_right_x, bottom_right_y = coords + + # Calculate missing coordinates: top right (x2, y1) and bottom left (x1, y2) + top_right = (bottom_right_x, top_left_y) + bottom_left = (top_left_x, bottom_right_y) + + # Ensure the order is [rectangle_id, [top_left], [top_right], [bottom_right], [bottom_left], other_info...] + top_left = (top_left_x, top_left_y) + bottom_right = (bottom_right_x, bottom_right_y) + + updated_rectangle = [rectangle_id, top_left, top_right, bottom_right, bottom_left] + others + updated_rectangles.append(updated_rectangle) + + return updated_rectangles + + def find_highest_image_number(self, json_data): + """ + Search through the provided JSON data for the highest number in the "Image_path". + + Args: + - j_data (List[Dict]): The input JSON data loaded into a Python data structure. + + Returns: + - int: The highest number found in the "Image_path"; returns -1 if no numbers are found. + """ + highest_number = -1 + with open(json_data, 'r') as file: + # Load the JSON data from the file + j_data = json.load(file) + pattern = re.compile(r'\d+') + + for item in j_data: + if "Image_path" in item: + # Extract numbers from "Image_path" + numbers = pattern.findall(item["Image_path"]) + for number in numbers: + # Convert found numbers to integers and track the highest + current_number = int(number) + if current_number > highest_number: + highest_number = current_number + highest_number+= 1 + return highest_number