diff --git a/scripts/Labeling.py b/scripts/Labeling.py new file mode 100644 index 0000000..20635b3 --- /dev/null +++ b/scripts/Labeling.py @@ -0,0 +1,610 @@ +import customtkinter as Ctk +import os +from PIL import Image, ImageTk, ImageDraw +import io +from scripts.SaveData import SaveData +import scripts.get_sys_info as system_code + + + +class Labeling(Ctk.CTkFrame): + def __init__(self,master,img_paths, output_path, callback, window_information, **kwargs): + super().__init__(master, **kwargs) + system_code.load_json_file() + self.data_saver = SaveData() + self.my_font = Ctk.CTkFont(family="Berlin Sans FB", size=22) + self.image_btn_size = system_code.btn_img_size + #callback + self.callback = callback + self.window_information = window_information + # Image resources + self.create_folder_image_path = r"./icons/create_folder.png" + self.open_folder_image_path = r"./icons/open_folder.png" + self.source_folder_image_path = r"./icons/source_folder.png" + # Load and resize image as before + self.create_folder_raw_image = Image.open(self.create_folder_image_path) + self.open_folder_raw_image = Image.open(self.open_folder_image_path) + self.source_folder_raw_image = Image.open(self.source_folder_image_path) + self.load_button_images() + # for the image show_process + self.image_scale_init() + # variables for the labeling itself + self.index = 0 # when this gets opened we always want to start a 0. + self.img_paths = img_paths + self.active_output_path = output_path + self.output_path = output_path + + # bounding boxes + self.labeling_boxes = [] + self.labeling_box_index = 0 + self.original_factor = None + self.img_factor_x = None + self.img_factor_y = None + self.resolution = None + self.labeled = False + self.mpenn_data = self.data_saver.search_in_folder(self.active_output_path) + self.save_index = self.data_saver.find_highest_image_number(self.mpenn_data[0]) + + self.bind("", self.save_and_load) + + self.start() + + def update_active_output_path(self, output_path): + self.active_output_path = output_path + self.mpenn_data = self.data_saver.search_in_folder(self.active_output_path) + self.save_index = self.data_saver.find_highest_image_number(self.mpenn_data[0]) + + def start(self): + self.create_labeling() + self.show_img(self.img_paths[self.index]) + + def open_new_folder(self): + """ + Callback for when a new folder is chosen. + """ + self.callback(self.output_path, 1) # Call the callback passing the current value + + def create_new_folder(self): + """ + Callback for when a new folder is chosen. + """ + self.callback(self.output_path, 0) # Call the callback passing the current value + + def return_image_information(self): + # Extract the directory of the image file + image_directory = os.path.dirname(self.img_paths[self.index]) + + # Now extract the folder name + desired_folder = os.path.basename(image_directory) + desired_name = os.path.basename(self.img_paths[self.index]) + + # Creating a tuple with the extracted information + text_information = (desired_name, desired_folder, self.active_output_path) + + # Presumed function to show or use the information + self.window_information(text_information) + + + def source_folder_dialog(self): + self.callback(None, 2) + + def image_scale_init(self): + self.original_image = None + self.displayed_image = None + self.dragging = False + self.resize_pending = False + self.after_id = None + self.tk_image = None + self.rect = None # Keep track of the rectangle element + self.start_x = None + self.start_y = None + self.end_x = None # Track the end position of the drag + self.end_y = None + self.image_position = (0, 0) # Initialize image position + self.tk_cropped_image = None + self.save_cropped = None + + + # the Labeling part + def create_labeling(self): + """adapt button size to window_size""" + self.big_canvas = Ctk.CTkCanvas(self,background="#5f00c7", bd=0, highlightthickness=0) + self.reset_btn = Ctk.CTkButton(self, text="reset Image", width=100, command=self.reset_rectangle, font=self.my_font) + self.create_folder_btn = Ctk.CTkButton(self,image=self.create_folder_image, text="", width=100, command=self.create_new_folder, font=self.my_font) + self.open_folder_btn = Ctk.CTkButton(self, image=self.open_folder_image,text="", width=100, command=self.open_new_folder, font=self.my_font) + self.save_img_btn = Ctk.CTkButton(self, text="Save Image", width=100, command=self.save_and_load, font=self.my_font) + self.delete_img_btn = Ctk.CTkButton(self, text="Delete Image", width=100, command=self.delete_img, font=self.my_font) + self.skip_time_btn = Ctk.CTkButton(self, text="Jump", width=100, command=self.skip_time, font=self.my_font) + self.new_source_btn = Ctk.CTkButton(self,image=self.source_folder_image, text="", width=100, command=self.source_folder_dialog, font=self.my_font) + self.preview_canvas = Ctk.CTkCanvas(self,background="#5f00c7", bd=0, highlightthickness=0) + self.start_mode = Ctk.StringVar(value=system_code.data_mode) + self.choose_mode = Ctk.CTkSegmentedButton(self, values= system_code.data_modes, + variable=self.start_mode, + command=self.set_data_mode,width=64,font=self.my_font) + + # big_canvas + self.big_canvas.bind("", self.on_press) + self.big_canvas.bind("", self.on_drag) + self.big_canvas.bind("", self.on_release) + self.after_idle(self.adjust_image_sizes_for_buttons) + self.bind("", self.on_resize) + self.place_labeling() + + def set_data_mode(self, value): + system_code.data_mode = value + print(system_code.data_mode) + if system_code.data_mode == "Resize": + self.reset_rectangle() + else: + if self.tk_cropped_image is not None: + self.show_img(self.save_cropped) + else: + self.show_img(self.img_paths[self.index]) + + + def adjust_image_sizes_for_buttons(self): + """Adjust the image sizes based on the current height of the buttons and update them accordingly.""" + self.update_idletasks() # Force Tkinter to finalize the window layout + + example_button_height = self.create_folder_btn.winfo_height() + example_button_width = self.create_folder_btn.winfo_width() + + # Assuming buttons' height as a basis for square image size + orientation_value = min(example_button_height, example_button_width) - 25 + + # Ensure the orientation_value does not drop below a minimum size threshold + orientation_value = max(orientation_value, 1) # Prevents size from being 0 or negative + + # Your existing logic to resize and apply the images + self.image_btn_size = (orientation_value, orientation_value) + self.load_button_images() + + self.create_folder_btn.configure(image=self.create_folder_image) + self.open_folder_btn.configure(image=self.open_folder_image) + self.new_source_btn.configure(image=self.source_folder_image) + + self.create_folder_btn.image = self.create_folder_image + self.open_folder_btn.image = self.open_folder_image + self.new_source_btn.image = self.source_folder_image + + + def load_button_images(self): + """ + Load, resize button images to the specified size and convert them into a Tkinter-compatible format. + """ + # Convert PIL images to Ctk.CTkImage objects for compatibility with Tkinter/customtkinter + self.create_folder_image = Ctk.CTkImage(self.create_folder_raw_image , size =self.image_btn_size) + self.open_folder_image = Ctk.CTkImage(self.open_folder_raw_image, size =self.image_btn_size) + self.source_folder_image = Ctk.CTkImage(self.source_folder_raw_image, size =self.image_btn_size) + + def reset_rectangle(self): + # Check if there is a rectangle to delete + if self.rect: + self.delete_current_rectangle(self.rect) + for rect in self.labeling_boxes: + self.delete_current_rectangle(rect[0]) + self.labeling_boxes = [] + self.labeling_box_index = 0 + self.rect = None + self.draw = None + self.preview_canvas.delete("all") + + def save_and_load(self): + save_path = f"{self.active_output_path}/{self.save_index:03d}{system_code.img_format}" + if system_code.data_mode == "Labeling": + if len(self.labeling_boxes) == 0: + self.labeled = False + else: + image = self.draw_rects(self.original_image) + image.save(save_path) + self.resolution = image.size + elif self.tk_cropped_image is not None: + self.save_cropped.save(save_path) + self.resolution = self.save_cropped.size + + + print( self.labeling_boxes) + self.data_saver.append_to_json_file(save_path, self.resolution, self.labeled, self.labeling_boxes, self.mpenn_data[0]) + self.reset_rectangle() + os.remove(self.img_paths[self.index]) + self.index += 1 + self.save_index += 1 + self.show_img(self.img_paths[self.index]) + + def draw_rects(self, image): + self.draw = ImageDraw.Draw(image) + + for rect in self.labeling_boxes: + # Each 'rect' in 'self.labeling_boxes' is expected to be a list with 2 elements: + # The first element could be a rectangle object or identifier (we will not use it here) + # The second element is a list of coordinates in the format [start_x, start_y, end_x, end_y] + coordinates = rect[1] # Getting the coordinates list + + color = rect[2] + thickness = rect[3] + if thickness <= 0: + thickness = 1 + self.draw.rectangle(coordinates, outline=color, width=thickness) + self.labeled = True + return image + + def delete_img(self): + os.remove(self.img_paths[self.index]) + self.index += 1 + self.show_img(self.img_paths[self.index]) + + def skip_time(self): + for i in range(system_code.skipable_frames): + os.remove(self.img_paths[self.index + i]) + self.index = self.index + system_code.skipable_frames + self.show_img(self.img_paths[self.index]) + + def show_img(self, to_displayed): + self.preview_canvas.delete("all") + self.tk_cropped_image = None + # Check if 'to_displayed' is a PIL Image object + if isinstance(to_displayed, Image.Image): + self.original_image = to_displayed # 'to_displayed' is already an image object, use it directly + else: + # It's assumed to be a file path, try to open as an image file + try: + self.original_image = Image.open(to_displayed) + except Exception as e: + print(f"Error opening image: {e}") + return + + self.return_image_information() + self.resize_image() + self.display_image() + + def resize_image(self): + """ + Resize the image to fit within the panel dimensions while maintaining aspect ratio. + Note: This function should be called after the window is visible and has been updated, to ensure + accurate dimensions are retrieved from 'big_canvas'. + """ + + self.big_canvas.update_idletasks() + + if self.original_image is None or self.big_canvas.winfo_width() <= 1 or self.big_canvas.winfo_height() <= 1: + return + # Update idletasks to ensure the window's layout is processed and accurate dimensions are retrieved + self.big_canvas.update_idletasks() + + panel_width, panel_height = self.big_canvas.winfo_width(), self.big_canvas.winfo_height() + + print("resized:", panel_width, panel_height) + original_width, original_height = self.original_image.size + aspect_ratio = original_width / original_height + + # Determine the best fit size for keeping the aspect ratio + if panel_width / aspect_ratio <= panel_height: + new_width, new_height = int(panel_width), int(panel_width / aspect_ratio) + else: + new_width, new_height = int(panel_height * aspect_ratio), int(panel_height) + + # Inside the resize_image method, after computing new_width and new_height + if new_width <= 0 or new_height <= 0: + return # Consider logging this case or handling it appropriately + + self.displayed_image = self.original_image.resize((new_width, new_height), Image.LANCZOS) + + # Calculate and print the size_factor + size_factor_width = original_width / new_width + size_factor_height = original_height / new_height + + self.img_factor_x = size_factor_width + self.img_factor_y = size_factor_height + print(f"Size Factor: {self.img_factor_x}, {self.img_factor_y}") + + def display_image(self): + """Display the image centered in the canvas.""" + if self.displayed_image is None: + return + + if self.tk_image: + self.delete_current_rectangle("all") # Clear the canvas before displaying a new image + + self.tk_image = ImageTk.PhotoImage(self.displayed_image) + # Force an update to ensure current dimensions are fetched + self.big_canvas.update_idletasks() + canvas_width, canvas_height = self.big_canvas.winfo_width(), self.big_canvas.winfo_height() + image_width, image_height = self.tk_image.width(), self.tk_image.height() + + # Calculate the center position + x_offset = int((canvas_width - image_width) // 2) + y_offset = int((canvas_height - image_height) // 2) + + self.big_canvas.create_image(x_offset, y_offset, anchor=Ctk.NW, image=self.tk_image) + self.image_position = (x_offset, y_offset) # Save the centered image position + + def on_press(self, event): + self.start_x = event.x + self.start_y = event.y + # If there's an existing rectangle, remove it + if self.rect and (system_code.data_mode == "Resize"): + self.delete_current_rectangle(self.rect) + # Create an initial 1x1 rectangle that will be adjusted in `on_drag` + self.rect = self.big_canvas.create_rectangle(self.start_x, self.start_y, self.start_x+1, self.start_y+1, + outline=system_code.color if system_code.data_mode != "Resize" else "#5f00c7", + width=system_code.thickness // self.img_factor_x if system_code.data_mode != "Resize" else 1) + + def on_resize(self, event): + """Handle window resize events with throttling.""" + if not self.resize_pending: + self.resize_pending = True + + if self.after_id: + self.after_cancel(self.after_id) + self.adjust_image_sizes_for_buttons() + self.after_id = self.after(100, self.perform_resize) + + def on_drag(self, event): + if not self.dragging: + # This marks the start of actual dragging, so we do the rectangle creation here + self.dragging = True # Set dragging flag to True to indicate dragging has started + # If there's an existing rectangle from a previous operation, remove it + if self.rect: + self.delete_current_rectangle(self.rect) + # Start a new rectangle + self.rect = self.big_canvas.create_rectangle(self.start_x, self.start_y, self.start_x+1, self.start_y+1, + outline=system_code.color if system_code.data_mode == "Labeling" else "#5f00c7", + width=system_code.thickness // self.img_factor_x if system_code.data_mode == "Labeling" else 1) + + self.end_x, self.end_y = event.x, event.y + + if system_code.data_mode == "Resize": + # Maintain 1:1 aspect ratio for resize mode + delta_x = self.end_x - self.start_x + delta_y = self.end_y - self.start_y + delta = min(abs(delta_x), abs(delta_y)) + delta_x = delta if delta_x > 0 else -delta + delta_y = delta if delta_y > 0 else -delta + + self.end_x, self.end_y = self.start_x + delta_x, self.start_y + delta_y + # For other modes, the rectangle adjusts to current coordinates directly + + # Update rectangle size during drag + self.big_canvas.coords(self.rect, self.start_x, self.start_y, self.end_x, self.end_y) + + def on_release(self, event): + # Only proceed if dragging actually occurred + if self.dragging: + self.big_canvas.coords(self.rect, self.start_x, self.start_y, self.end_x, self.end_y) + if self.start_x > self.end_x: + temp = self.start_x + self.start_x = self.end_x + self.end_x = temp + if self.start_y > self.end_y: + temp = self.start_y + self.start_y = self.end_y + self.end_y = temp + self.big_canvas.coords(self.rect, self.start_x, self.start_y, self.end_x, self.end_y) + print(f"Rectangle coordinates: Start({self.start_x}, {self.start_y}) End({self.end_x}, {self.end_y})") + self.display_cropped_part() # New line to display the cropped area + self.dragging = False # Reset dragging flag + if system_code.data_mode == "Labeling" and self.rect is not None: + coordinates = [self.start_x, self.start_y, self.end_x, self.end_y] + coordinates = self.subtract_image_from_canvas(coordinates) + coordinates = self.multiply_array(coordinates, self.img_factor_x, self.img_factor_y) + temp_array = [self.labeling_box_index,coordinates, system_code.color, system_code.thickness] + self.labeling_boxes.append(temp_array) + self.labeling_box_index += 1 + else: + # Handle the case where a rectangle shouldn't have been started + if self.rect: + self.delete_current_rectangle(self.rect) + self.rect = None + + def multiply_array(self, arr, num1, num2): + """ + Multiplies each element of the array 'arr' by 'num'. + + Parameters: + arr (list): The input list whose elements are to be multiplied. + num (int): The number by which each element of the list is multiplied. + + Returns: + list: A new list with each element of 'arr' multiplied by 'num'. + """ + # Use list comprehension to multiply each element by num + return arr[0] * num1, arr[1]* num2, arr[2]* num1, arr[3] * num2 + + def subtract_image_from_canvas(self, arr): + + width = self.image_position[0] + height = self.image_position[1] + + return arr[0] - width, arr[1] - height, arr[2] - width, arr[3] - height + + + def display_cropped_part(self): + if not self.validate_image_and_rectangle(): + return + + adj_coordinates = self.adjust_coordinates_for_border() + if not self.validate_adjusted_coordinates(adj_coordinates): + self.delete_current_rectangle(self.rect) + return + + crop_coordinates = self.calculate_crop_coordinates(adj_coordinates) + self.cropped_image = self.original_image.crop(crop_coordinates) + self.display_resized_cropped_image(self.cropped_image) + + def adjust_coordinates_for_border(self): + """Adjust rectangle's position by considering the image's position and border width.""" + adj_start_x = self.start_x - self.image_position[0] + (system_code.thickness / 2) + adj_start_y = self.start_y - self.image_position[1] + (system_code.thickness / 2) + adj_end_x = self.end_x - self.image_position[0] - (system_code.thickness / 2) + adj_end_y = self.end_y - self.image_position[1] - (system_code.thickness / 2) + return adj_start_x, adj_start_y, adj_end_x, adj_end_y + + def validate_adjusted_coordinates(self, coordinates): + """Ensure logical and boundary adherence of adjusted coordinates.""" + adj_start_x, adj_start_y, adj_end_x, adj_end_y = coordinates + if (adj_end_x <= adj_start_x) or (adj_end_y <= adj_start_y): + # Negative width or height indicates a coordinate error + return False + + if (adj_start_x < 0 or adj_start_y < 0 or + adj_end_x > self.displayed_image.width or + adj_end_y > self.displayed_image.height): + # Coordinates exceed displayed image bounds + return False + + return True + + def calculate_crop_coordinates(self, adj_coordinates): + """Calculate crop coordinates on the original image, considering display scaling.""" + adj_start_x, adj_start_y, adj_end_x, adj_end_y = adj_coordinates + + # Scaling ratios, considering the displayed image might be scaled down/up to fit into the canvas + x_scale = self.original_image.size[0] / self.displayed_image.width + y_scale = self.original_image.size[1] / self.displayed_image.height + + # Apply scaling to adjust coordinates to match the original image size + crop_start_x = max(0, int(adj_start_x * x_scale)) + crop_start_y = max(0, int(adj_start_y * y_scale)) + crop_end_x = min(self.original_image.size[0], int(adj_end_x * x_scale)) + crop_end_y = min(self.original_image.size[1], int(adj_end_y * y_scale)) + + return crop_start_x, crop_start_y, crop_end_x, crop_end_y + + def display_resized_cropped_image(self, cropped_image): + """Resize and display the cropped image on the preview canvas.""" + cropped_image_aspect_ratio = (cropped_image.width / cropped_image.height) + + preview_canvas_width = self.preview_canvas.winfo_width() + preview_canvas_height = self.preview_canvas.winfo_height() + if cropped_image_aspect_ratio > (preview_canvas_width / preview_canvas_height): + new_width = preview_canvas_width + new_height = int(preview_canvas_width / cropped_image_aspect_ratio) + else: + new_height = preview_canvas_height + new_width = int(preview_canvas_height * cropped_image_aspect_ratio) + + resized_cropped_image = cropped_image.resize((new_width, new_height), Image.LANCZOS) + self.save_cropped = resized_cropped_image + self.tk_cropped_image = ImageTk.PhotoImage(resized_cropped_image) + self.preview_canvas.delete("all") + centered_x = (preview_canvas_width - new_width) / 2 + centered_y = (preview_canvas_height - new_height) / 2 + self.preview_canvas.create_image(centered_x, centered_y, anchor='nw', image=self.tk_cropped_image) + self.preview_canvas.image = self.tk_cropped_image + + def perform_resize(self): + """Perform the resize operation and clear cropped image and rectangle.""" + if self.resize_pending: + self.resize_image() + self.display_image() + + # Clear the rectangle on the main canvas if it exists + self.reset_rectangle() + + # Reset rectangle coordinates + self.start_x, self.start_y, self.end_x, self.end_y = None, None, None, None + + # Optionally, if desired, clear the dragging states + self.dragging = False + + # Now print the image position and size if an image is displayed, else, print "No image displayed." + if self.displayed_image: + image_width, image_height = self.displayed_image.size + print(f"Image position: {self.image_position}, Size: {image_width}x{image_height}") + + + + def delete_current_rectangle(self, obj): + """Remove the rectangle object if coordinates are invalid.""" + self.big_canvas.delete(obj) + self.rect = None + + def validate_image_and_rectangle(self): + if not self.displayed_image or not self.rect: + return False + return True + + def place_labeling(self): + self.big_canvas.place( + relx=0.025, + rely=0.5, + relwidth=0.775, + relheight=0.95, + anchor="w" + ) + self.reset_btn.place( + relx=0.975, + rely=0.1, + relwidth=0.15, + relheight=0.06, + anchor="e", + ) + self.create_folder_btn.place( + relx=0.975, + rely=0.2, + relwidth=0.06, + relheight=0.06, + anchor="e", + ) + self.open_folder_btn.place( + relx=0.825, + rely=0.2, + relwidth=0.06, + relheight=0.06, + anchor="w", + ) + self.save_img_btn.place( + relx=0.975, + rely=0.3, + relwidth=0.15, + relheight=0.06, + anchor="e", + ) + self.delete_img_btn.place( + relx=0.975, + rely=0.4, + relwidth=0.15, + relheight=0.06, + anchor="e", + ) + self.skip_time_btn.place( + relx=0.975, + rely=0.5, + relwidth=0.06, + relheight=0.06, + anchor="e", + ) + self.new_source_btn.place( + relx=0.825, + rely=0.5, + relwidth=0.06, + relheight=0.06, + anchor="w", + ) + self.preview_canvas.place( + relx=0.975, + rely=0.7, + relwidth=0.15, + relheight=0.25, + anchor="e", + ) + self.choose_mode.place( + relx=0.975, + rely=0.9, + relwidth=0.15, + relheight=0.06, + anchor="e", + ) + + def hide_labeling(self): + self.big_canvas.place_forget() + self.reset_btn.place_forget() + self.create_folder_btn.place_forget() + self.open_folder_btn.place_forget() + self.save_img_btn.place_forget() + self.delete_img_btn.place_forget() + self.skip_time_btn.place_forget() + self.new_source_btn.place_forget() + self.preview_canvas.place_forget() + self.choose_mode.place_forget() +