diff --git a/SOFIA.py b/SOFIA.py index 87431bc..a662415 100644 --- a/SOFIA.py +++ b/SOFIA.py @@ -14,37 +14,173 @@ from math import log10, floor, pi import numpy.polynomial.polynomial as poly -#SOFIA Size of Oxidation Feature from Image Analysis -#Allows the user to load an image and quickly crop, threshold, and detect edges to isolate lines for the purpose -#of measuring scale thicknesses for internal and external oxidation. -#Created by Padraig Stack -#Last modified December 26, 2020 - +""" +SOFIA Size of Oxidation Feature from Image Analysis +Allows the user to load an image and quickly crop, threshold, and detect edges to isolate lines for the purpose +of measuring scale thicknesses for internal and external oxidation. +Created by Padraig Stack +Last modified June 16, 2021 + Note if a function isn't working right check an older version (there was a function that appeared to be obsolete) +""" pytesseract.pytesseract.tesseract_cmd = "C:\Program Files\Tesseract-OCR\\tesseract.exe" ##Location of the text reading software -path = cbtn = directory = filename = orig_img = crop_menu = scale_menu = scale_canvas = y_lower_crop = y_upper_crop = x_lower_crop = x_upper_crop = scale_leftmost = scale_rightmost = ratio = scale_warning = scale_ratio = poly_menu = None -lower_crop = upper_crop = cropping_image = crop_close = thresh_image = thresh_btn = crop = scale_crop_img = scale_select_menu = scale_select_canvas = set_scale_image = set_scale_img = scale_contours = scale_compute_btn = scale_entry = None -thresh_menu = lower_thresh = upper_thresh = thresh_panel_new = thresh_panel_old = lower_thresh_old = upper_thresh_old = None -thresh_close_btn = edge_btn = contour_iterations = edge_menu = contour_menu = save_name_entry = save_name_label = poly_canvas = poly_canvas_image = poly_img_resize = None -contour_input= lower_list = upper_list = csvlx = csvux = csvly = csvuy = tracing_img = tracing_image = contour_close_btn = None -save_csv_btn = edge_close_btn = short_dist_btn = undo_btn = list_label = crop_status = thresh_status = contour_status = calculate_status = internal_threshold_label = None -cc_calc_btn = cc_contour_check_btn = mag_status = int_calc_btn = crop_canvas = thresh_canvas = thresh_img = internal_threshold = click_contours = click_index = click_tuple = contour_tuple = low_slide = up_slide = internal_list = None - -class imported_image: - def __init__(self, orig_img, in_or_ex): - width = orig_img.shape[1] - ratio = width // 1000 - if ratio == 0: #Prevents divide by zero errors - ratio = int(1) - self.ratio = ratio - if in_or_ex == "ex": - self.in_or_ex = 1 - else: - self.in_or_ex = 0 - self.values() +font = ("Helvetica", 14) +font2 = ("Helvetica", 12) + +class MainWindow: + + #Establish the main menu + + def __init__(self,main_menu): ##Initial Menu + self.main_menu = main_menu + self.main_menu.title("Main Menu") + self.main_menu.configure(bg="gray69") + + Label(self.main_menu, text = "Please select the following menus to use", font = font).grid(row = 0, columnspan = 2) + self.external_oxidation_menu_btn = Button(self.main_menu, text="External Oxidation (CC Prep)", font = font, bg="skyblue", command = self.external_oxidation_menu) + self.external_oxidation_menu_btn.grid(row = 1, column = 0) + Label(main_menu, text="This will let you calculate the shortest distance between select contours\nor prepare samples for the concavity calculator", font = font2).grid(row =1, column = 1, sticky="W") + + self.concavity_menu_btn = Button(self.main_menu, text="Concavity Calculations", font = font, bg="slate blue", command = self.concavity_menu) + self.concavity_menu_btn.grid(row = 2, column = 0) + Label(main_menu, text="This will let you find concavity of the lower line. \nCompares thicknesses at concave and concave regions", font = font2).grid(row=2, column = 1, sticky="W") + + self.internal_oxidation_menu_btn = Button(self.main_menu, text="Internal Oxidation", font= font, bg="purple2", command = self.internal_menu) + self.internal_oxidation_menu_btn.grid(row=3, column=0) + Label(main_menu, text="Prepare CSVs to measure internal oxidation\nAlso measures continuity in slices", font = font2).grid(row=3, column=1, sticky="W") + + self.exit_btn = Button(self.main_menu, text="Exit Program", font= font, bg="tomato", command = self.main_menu.destroy) + self.exit_btn.grid(row=4, column=0) + + def reset_button(self): ##Resets the menu to start again + if "cbtn" in dir(self): + self.cbtn.destroy() + if "thresh_btn" in dir(self): + self.thresh_btn.destroy() + if "edge_btn" in dir(self): + self.edge_btn.destroy() + if "save_csv_btn" in dir(self): + self.save_csv_btn.destroy() + if "crop_status" in dir(self): + self.crop_status.grid_remove() + if "contour_status" in dir(self): + self.contour_status.grid_remove() + if "save_name_entry" in dir(self): + self.save_name_entry.grid_remove() + if "save_name_label" in dir(self): + self.save_name_label.grid_remove() + if "scale_ratio" in dir(self): + del self.scale_ratio + if "scale_status" in dir(self): + del self.scale_status + self.status_path.configure(text="There is no loaded image", font=font2) + self.status_path.update() + + def external_oxidation_menu(self): ##Creates the main menu for external oxidation image analysis + self.in_or_ex = 1 + self.root = Toplevel() + self.main_menu.withdraw() + self.root.title("Auto Measurement") + self.root.configure(bg="gray69") + self.root.minsize(300,300) + + self.path_btn = Button(self.root, text="Please select the image you would like to use", font = font, bg="OliveDrab1", command = self.select_image) + self.path_btn.grid(row = 0, column = 0) + self.status_path = Label(self.root, text="There is no loaded image", font = font2) + self.status_path.grid(row = 0, column = 1) + + self.current_dir = os.getcwd() + self.unworked_dir = self.current_dir+"/unworkedcsv/" + check_dir = Path(self.unworked_dir) + if check_dir.exists() is False: + os.mkdir(unworked_dir) + os.mkdir(current_dir+"/workedcsv") + waiting_files = len(os.listdir(self.unworked_dir))//3 + if waiting_files > 0: + self.short_dist_btn = Button(self.root, text="Calculate Shortest Distance", font = font, bg="dodger blue", command = self.shortest_distance) + self.short_dist_btn.grid(row=6, column=0) + self.calculate_status = Label(self.root, text="There are "+str(waiting_files)+" images waiting for calculations", font = font2) + self.calculate_status.grid(row =6, column = 1, sticky="W") + self.reset_btn = Button(self.root, text="Reset", font=font, command=lambda:[self.reset_button()]) + self.reset_btn.grid(row=8, column=0) + self.return_btn = Button(self.root, text="Return to Main Menu", font = font, bg="linen", command = lambda:[self.root.destroy(), self.main_menu.deiconify()]) + self.return_btn.grid(row = 8, column= 1) + + def concavity_menu(self): ##Uses CSV data from internal/external measurements to measure concavities of the oxide interface + self.cc_menu = Toplevel() + self.main_menu.withdraw() + self.cc_menu.title("Concavity Calculator") + self.cc_menu.configure(bg="gray69") + self.cc_menu.minsize(300,300) + + + return_btn = Button(self.cc_menu, text="Return to Main Menu", font = font, bg="linen", command = lambda:[self.cc_menu.destroy(), self.main_menu.deiconify()]) + return_btn.grid(row = 6, columnspan = 2) + + csv_select = Button(self.cc_menu, text="Select CSVs", font=font, bg="deep sky blue", command = self.load_csv) + csv_select.grid(row = 1, column = 0) + self.cc_csv_status = Label(self.cc_menu, text="Select the csv you would like to use for calculations", font=font2) + self.cc_csv_status.grid(row=1,column = 1) - def values(self, pct = 2): #Recallable dynamic resolution function - monitor_width = root.winfo_screenwidth() + def internal_menu(self): ##Creates the main menu for internal oxidation image analysis + self.in_or_ex = 0 + self.root = Toplevel() + self.main_menu.withdraw() + self.root.title("Internal Oxidation") + self.root.configure(bg="gray69") + self.root.minsize(300,300) + + path_btn = Button(self.root, text="Please select the image you would like to use", font = font, bg="OliveDrab1", command = self.select_image) + path_btn.grid(row = 0, column = 0) + self.status_path = Label(self.root, text="There is no loaded image", font = font2) + self.status_path.grid(row = 0, column = 1) + + self.current_dir = os.getcwd() + worked_dir = self.current_dir+"/worked-internalcsv/" + check_dir = Path(worked_dir) + if check_dir.exists() is False: + os.mkdir(worked_dir) + + reset_btn = Button(self.root, text="Reset", font=font, command=self.reset_button) + reset_btn.grid(row=8, column=0) + return_btn = Button(self.root, text="Return to Main Menu", font = font, bg="linen", command = lambda:[self.root.destroy(), self.main_menu.deiconify()]) + return_btn.grid(row = 8, column = 1) + + #Internal and External functions + #Image selection/property generation + def select_image(self): ##Allows the user to select an image to analyse + self.path = filedialog.askopenfilename(filetypes = [("Image", ".bmp"), ("Image", ".tif"), ("Image", ".jpg"), ("Image", ".png")]) + if len(self.path) > 0: + self.directory = os.path.dirname(self.path) + self.filename = os.path.basename(self.path) + self.orig_img = cv2.imread(self.path, 0) + self.img_width = self.orig_img.shape[1] + self.img_height = self.orig_img.shape[0] + if self.img_width > self.img_height: + self.image_ratio = self.img_width // 1000 + else: + self.image_ratio = self.img_height // 1000 + if self.image_ratio == 0: + self.image_ratio = int(1) + self.scale_values() + self.center_circle = int(self.image_ratio) + self.label_text = int((self.image_ratio//2)+1) + self.scale_bar = int(round_to_1(self.img_width)/10) + image_area = self.img_width * self.img_height + self.area_thresh = int(image_area * .00004) + self.contour_buffer = int(self.image_ratio*3.125) + self.crackbuffer = int(5) + self.status_path.configure(text=("The current loaded image is: " + self.filename), font=font2) + self.status_path.update() + if "cbtn" not in dir(self): + self.cbtn = Button(self.root, text="Crop the image", bg="PaleTurquoise1", font=font, command = self.crop_image) + self.cbtn.grid(row = 1, column = 0) + else: + self.status_path.configure(text="You have not selected a new image, please select one", font=font2) + self.status_path.update() + + def scale_values(self, pct=2): ##Recallable dynamic resolution function + monitor_width = self.root.winfo_screenwidth() #Obtains screen size for scaling purposes if pct != 2: if pct <= 0.3: modifier = 1 @@ -60,931 +196,682 @@ def values(self, pct = 2): #Recallable dynamic resolution function modifier = 1 if self.in_or_ex == 1: if monitor_width >= 1920 and monitor_width < 2560: #Adjusts the image sizes based off monitor resolution width (assumed 16:9) - self.fullsize = int((75/self.ratio)*modifier) - self.crop_resize = int((135/self.ratio)*modifier) - self.croped_resize = int((165/self.ratio)*modifier) - self.contour_resize = int((140/self.ratio)*modifier) - self.thresh_width = int((165*self.ratio)) + self.fullsize = int((75/self.image_ratio)*modifier) + self.crop_resize = int((135/self.image_ratio)*modifier) + self.croped_resize = int((165/self.image_ratio)*modifier) + self.contour_resize = int((140/self.image_ratio)*modifier) + self.thresh_width = int((165*self.image_ratio)) elif monitor_width < 1920: - self.fullsize = int((50/self.ratio)*modifier) - self.crop_resize = int((90/self.ratio)*modifier) - self.croped_resize = int((110/self.ratio)*modifier) - self.contour_resize = int((80/self.ratio)*modifier) - self.thresh_width = int(110*self.ratio) + self.fullsize = int((50/self.image_ratio)*modifier) + self.crop_resize = int((90/self.image_ratio)*modifier) + self.croped_resize = int((110/self.image_ratio)*modifier) + self.contour_resize = int((80/self.image_ratio)*modifier) + self.thresh_width = int(110*self.image_ratio) else: - self.fullsize = int((100/self.ratio)*modifier) - self.crop_resize = int((180/self.ratio)*modifier) - self.croped_resize = int((220/self.ratio)*modifier) - self.contour_resize = int((160/self.ratio)*modifier) - self.thresh_width = int(220*self.ratio) - self.vertical_check = int(4*self.ratio) - self.edge_limit = int(25*self.ratio) + self.fullsize = int((100/self.image_ratio)*modifier) + self.crop_resize = int((180/self.image_ratio)*modifier) + self.croped_resize = int((220/self.image_ratio)*modifier) + self.contour_resize = int((160/self.image_ratio)*modifier) + self.thresh_width = int(220*self.image_ratio) + self.vertical_check = int(4*self.image_ratio) + self.edge_limit = int(25*self.image_ratio) else: if monitor_width >= 1920 and monitor_width < 2560: #Adjusts the image sizes based off monitor resolution width (assumed 16:9) - self.fullsize = int((70/self.ratio)*modifier) - self.crop_resize = int((150/self.ratio)*modifier) - self.croped_resize = int((150/self.ratio)*modifier) - self.contour_resize = int((105/self.ratio)*modifier) - self.thresh_width = int(150*self.ratio) + self.fullsize = int((70/self.image_ratio)*modifier) + self.crop_resize = int((150/self.image_ratio)*modifier) + self.croped_resize = int((150/self.image_ratio)*modifier) + self.contour_resize = int((105/self.image_ratio)*modifier) + self.thresh_width = int(150*self.image_ratio) elif monitor_width < 1920: - self.fullsize = int((45/self.ratio)*modifier) - self.crop_resize = int((100/self.ratio)*modifier) - self.croped_resize = int((100/self.ratio)*modifier) - self.contour_resize = int((70/self.ratio)*modifier) - self.thresh_width = int(100*self.ratio) + self.fullsize = int((45/self.image_ratio)*modifier) + self.crop_resize = int((100/self.image_ratio)*modifier) + self.croped_resize = int((100/self.image_ratio)*modifier) + self.contour_resize = int((70/self.image_ratio)*modifier) + self.thresh_width = int(100*self.image_ratio) else: - self.fullsize = int((90/self.ratio)*modifier) - self.crop_resize = int((160/self.ratio)*modifier) - self.croped_resize = int((200/self.ratio)*modifier) - self.contour_resize = int((140/self.ratio)*modifier) - self.thresh_width = int(200*self.ratio) - self.vertical_check = int(2*self.ratio) - self.edge_limit = int(3*self.ratio) - - self.center_circle = int(self.ratio) - self.label_text = int((self.ratio//2)+1) - self.scale_bar = int(round_to_1(orig_img.shape[1])/10) - self.image_area = orig_img.shape[1] * orig_img.shape[0] - self.area_thresh = int(self.image_area * .00004) - self.contour_buffer = int(self.ratio*3.125) - self.crackbuffer = int(5) - - -def Click(event): #Updates the entry in contour_input for easier contour adding - global click_contours, contour_tuple - mouse_x, mouse_y = event.x, event.y - mouse_x = mouse_x * (100/img_class.contour_resize) - mouse_y = mouse_y * (100/img_class.contour_resize) - m_array = [(mouse_x, mouse_y)] - - near_contours = [(x,y) for x, y in click_tuple if (x in range(int(mouse_x - (img_class.ratio*20)), int(mouse_x + (img_class.ratio*20))) and y in range(int(mouse_y - (img_class.ratio*20)), int(mouse_y + (img_class.ratio*20))))] - if len(near_contours) > 0: - near_index = closest_node(m_array, near_contours) - close_point = near_contours[near_index] - contour_index = click_tuple.index(close_point) - contour_number = click_index[contour_index] - contour_input.delete(0,"end") - contour_input.insert(END, contour_number) - -def click_ratio(event): - mouse_x, mouse_y = event.x, event.y - mouse_x = mouse_x - mouse_y = mouse_y - m_array = [(mouse_x, mouse_y)] - - near_contours = [(x,y) for x, y in click_tuple if (x in range(int(mouse_x - (img_class.ratio*20)), int(mouse_x + (img_class.ratio*20))) and y in range(int(mouse_y - (img_class.ratio*20)), int(mouse_y + (img_class.ratio*20))))] - if len(near_contours) > 0: - near_index = closest_node(m_array, near_contours) - close_point = near_contours[near_index] - contour_index = click_tuple.index(close_point) - contour_number = click_index[contour_index] - scale_contour_input.delete(0,"end") - scale_contour_input.insert(END, contour_number) - -def closest_node(node, nodes): #Finds the index in a tuple of tuples closest to a given tuple - nodes = np.asarray(nodes) - deltas = nodes - node - dist_2 = np.einsum("ij,ij->i", deltas, deltas) - return np.argmin(dist_2) - - -def resize(Original, sizepercentage): ##Resizes an opencv image array - width = int(Original.shape[1] * sizepercentage / 100) - height = int(Original.shape[0] * sizepercentage / 100) - dsize = (width, height) - return cv2.resize(Original, dsize) + self.fullsize = int((90/self.image_ratio)*modifier) + self.crop_resize = int((160/self.image_ratio)*modifier) + self.croped_resize = int((200/self.image_ratio)*modifier) + self.contour_resize = int((140/self.image_ratio)*modifier) + self.thresh_width = int(200*self.image_ratio) + self.vertical_check = int(2*self.image_ratio) + self.edge_limit = int(3*self.image_ratio) + + #Crop menu and scale bar determining + def crop_image(self): ##Creates a menu to crop the image. Also contains the feature to set a manual scale + if "crop_menu" in dir(self): + self.crop_menu.destroy() + if "crop_close" in dir(self): + del self.crop_close + self.crop_menu = Toplevel() + self.crop_menu.title=("Cropping Menu") + self.crop_menu.configure(bg="gray69") + self.crop_menu.minsize(300,300) + + Label(self.crop_menu, text=("The image you have loaded is " +str(self.img_height) +" pixels tall" + "\nAdjust the parameters until the entire scale is in the frame"), bg="thistle1", font=font2).grid(row = 0, columnspan = 2) + + orig_scale = self.orig_img.copy() #Creates a scale on the side of the image to help with cropping + for i in range(self.img_height//self.scale_bar): + cv2.putText(orig_scale, str(i*self.scale_bar), (0, (self.img_height-(i*self.scale_bar))), cv2.FONT_HERSHEY_SIMPLEX, (self.label_text+2), (255,255,255),(self.center_circle+3)) + cv2.putText(orig_scale, "____", (0, (self.img_height-(i*self.scale_bar))), cv2.FONT_HERSHEY_SIMPLEX, (self.label_text+2), (255,255,255),(self.center_circle)) + + + orig_resize = resize(orig_scale, self.fullsize) #Prepares and adds an image to the GUI + resize_width = orig_resize.shape[1] + resize_height = orig_resize.shape[0] + orig_resize = ImageTk.PhotoImage(Image.fromarray(orig_resize)) + self.crop_canvas = Canvas(self.crop_menu, width=resize_width, height=resize_height) + self.crop_canvas.grid(row = 10, columnspan = 3) + self.cropping_image= self.crop_canvas.create_image(0,0, image= orig_resize, anchor=NW) + self.crop_canvas.update() + + Label(self.crop_menu, text="Lower Height", font= font2).grid(row = 2, column = 0, sticky = "E") #Entry for lower height + self.lower_crop = Scale(self.crop_menu, from_=1, to=self.img_height, orient = HORIZONTAL, length=400, command= self.crop_update) + self.lower_crop.set(0) + self.lower_crop.grid(row = 2, column = 1, columnspan = 2, sticky = "W") + + Label(self.crop_menu, text="Upper Height", font= font2).grid(row = 1, column = 0, sticky = "E") #Entry for upper height + self.upper_crop = Scale(self.crop_menu, from_=0, to=(self.img_height-1), orient = HORIZONTAL, length=400, command= self.crop_update) + self.upper_crop.set(self.img_height) + self.upper_crop.grid(row = 1, column = 1, columnspan = 2, sticky = "W") + + if "scale_ratio" not in dir(self): + ratio = scale_reader(self.path) + if ratio == "Error": + self.scale_status = Label(self.crop_menu, text="The scale ratio was not determined automatically.\nPlease determine it using on of the buttons.Adjust crop if necessary", font=font2) + self.scale_status.grid(row = 6, column = 0, columnspan = 2) -def round_to_1(x): - return round(x, -int(floor(log10(abs(x))))) + else: + self.scale_status = Label(self.crop_menu, text="The scale ratio has been determined automatically!\nYou can override using buttons. Adjust crop if necessary", font=font2) + self.scale_status.grid(row = 6, column = 0, columnspan = 2) + self.scale_ratio = ratio + else: + self.scale_status = Label(self.crop_menu, text="The scale ratio from the previous image is currently being used.\nOverride using the other scale options. ADjust crop if necessary", font=font2) + self.scale_status.grid(row=6, column = 0, columnspan = 2) + scale_reader_redo_btn = Button(self.crop_menu, text="Redo Auto Scale Reader", bg="MediumOrchid2", font=font2, command = self.scale_reader_redo) + scale_reader_redo_btn.grid(row = 7, column = 0) + + scale_reader_btn = Button(self.crop_menu, text="Scale Bar Selection", bg="lawn green", font=font2, command = lambda: self.scale_reader_manual(resize_width)) + scale_reader_btn.grid(row =7, column = 1) + self.scale_manual_entry = Entry(self.crop_menu, font=font2) + self.scale_manual_entry.insert(END, "Enter the scale ratio here") + self.scale_manual_entry.grid(row = 6, column = 2, sticky="S") + self.scale_entry_btn = Button(self.crop_menu, text="Use Value in Entry (px/um)", bg="dark orange", font=font2, command = self.set_scale_manual_entry) + self.scale_entry_btn.grid(row=7, column=2) + + def crop_update(self, x): ##Reads the values of the crop sliders to update the image displayed in the crop menu + self.lower_crop_val = int(float(self.img_height)-float(self.lower_crop.get())) + self.upper_crop_val = int(float(self.img_height)-float(self.upper_crop.get())) + + self.crop = self.orig_img.copy() + for i in range(self.img_height//self.scale_bar): + cv2.putText(self.crop, str(i*self.scale_bar), (0, (self.img_height-(i*self.scale_bar))), cv2.FONT_HERSHEY_SIMPLEX, (self.label_text+2), (255,255,255),(self.center_circle+2)) + cv2.putText(self.crop, "____", (0, (self.img_height-(i*self.scale_bar))), cv2.FONT_HERSHEY_SIMPLEX, (self.label_text+2), (255,255,255),(self.center_circle)) + self.crop = self.crop[self.upper_crop_val:self.lower_crop_val, 0:self.img_width] + + crop_pct = (self.lower_crop_val - self.upper_crop_val)/self.img_height + self.scale_values(crop_pct) + + self.crop_resize = resize(self.crop, int((75/self.image_ratio))) + resize_width = self.crop_resize.shape[1] + resize_height = self.crop_resize.shape[0] + self.crop_canvas.config(width=resize_width, height=resize_height) + self.crop_resize = ImageTk.PhotoImage(Image.fromarray(self.crop_resize)) + self.crop_canvas.itemconfigure(self.cropping_image, image=self.crop_resize) + self.crop_canvas.update() + + if "crop_close" not in dir(self): + self.crop_close = Button(self.crop_menu, text = "Close and Continue", bg = "tomato", font= font2, command = self.crop_menu.destroy) + self.crop_close.grid(row = 9, column = 1) + self.thresh_btn = Button(self.root, text="Adjust Thresholds", font = font, bg="DeepSkyBlue2", command = self.thresh_image) + self.thresh_btn.grid(row = 2, column = 0) + self.crop_status = Label(self.root, text = "Crop has been updated", font = font) + self.crop_status.grid(row = 1, column = 1, sticky = "W") + + def scale_reader_manual(self, resize_width): ##Manual method to determine a scale. Crops the full image to isolate the scale bar + if "scale_menu" in dir(self): + self.scale_menu.destroy() + self.crop_menu.withdraw() + self.scale_menu = Toplevel() + self.scale_menu.title=("Scale Cropping Menu") + self.scale_menu.configure(bg="gray69") + self.scale_menu.minsize(resize_width, 300) + + Label(self.scale_menu, text=("Adjust the height until the scale bar is isolated"), bg="thistle1", font=font2).grid(row = 0, columnspan = 2) + + orig_scale = self.orig_img.copy() #Creates a scale on the side of the image to help with cropping + orig_resize = resize(orig_scale, self.fullsize) #Prepares and adds an image to the GUI + resize_width = orig_resize.shape[1] + resize_height = orig_resize.shape[0] + orig_resize = ImageTk.PhotoImage(Image.fromarray(orig_resize)) + self.scale_canvas = Canvas(self.scale_menu, width=resize_width, height=resize_height) + self.scale_canvas.grid(row = 5, column = 1, columnspan = 2) + self.scale_crop_img = self.scale_canvas.create_image(0,0, image= orig_resize, anchor=NW) + self.scale_canvas.update() + -def select_image(in_or_ex = "ex"): ##Allows the user to select an image from their computer to use in future steps - global path, cbtn, directory, filename, orig_img, img_class - path = filedialog.askopenfilename() - if len(path) > 0: - directory = os.path.dirname(path) - filename = os.path.basename(path) - orig_img = cv2.imread(path, 0) - img_class = imported_image(orig_img, in_or_ex) - status_path.configure(text=("The current loaded image is: " + filename), font=("Helvetica", 12)) - status_path.update() - if cbtn is None: - cbtn = Button(root, text="Crop the image", bg="PaleTurquoise1", font=("Helvetica", 14), command = crop_image) - cbtn.grid(row = 1, column = 0) - else: - status_path.configure(text="You have not selected a new image, please select one", font=("Helvetica", 12)) - status_path.update() - -def crop_image(): ##Creates a menu to crop the image - global crop_menu, lower_crop, upper_crop, cropping_image, crop_canvas, crop_close, scale_ratio - if crop_menu is not None: - crop_menu.destroy() - crop_close = None - - crop_menu = Toplevel() - crop_menu.title=("Cropping Menu") - crop_menu.configure(bg="gray69") - crop_menu.minsize(300,300) - - Label(crop_menu, text=("The image you have loaded is " +str(orig_img.shape[0]) +" pixels tall" - "\nAdjust the parameters until the entire scale is in the frame"), bg="thistle1", font=("Helvetica", 12)).grid(row = 0, columnspan = 2) - - orig_scale = orig_img.copy() #Creates a scale on the side of the image to help with cropping - height = int(orig_img.shape[0]) - for i in range(height//img_class.scale_bar): - cv2.putText(orig_scale, str(i*img_class.scale_bar), (0, (height-(i*img_class.scale_bar))), cv2.FONT_HERSHEY_SIMPLEX, (img_class.label_text+2), (255,255,255),(img_class.center_circle+3)) - cv2.putText(orig_scale, "____", (0, (height-(i*img_class.scale_bar))), cv2.FONT_HERSHEY_SIMPLEX, (img_class.label_text+2), (255,255,255),(img_class.center_circle)) + Label(self.scale_menu, text="Lower Height", font= font2).grid(row = 2, column = 0, sticky = "E") #Entry for lower height + self.y_lower_crop = Scale(self.scale_menu, from_=0, to=self.img_height, orient = HORIZONTAL, length=400, command= self.scale_update) + self.y_lower_crop.set(0) + self.y_lower_crop.grid(row = 2, column = 1, sticky = "W") + + Label(self.scale_menu, text="Upper Height", font= font2).grid(row = 1, column = 0, sticky = "E") #Entry for upper height + self.y_upper_crop = Scale(self.scale_menu, from_=0, to=self.img_height, orient = HORIZONTAL, length=400, command= self.scale_update) + self.y_upper_crop.set(self.img_height) + self.y_upper_crop.grid(row = 1, column = 1, sticky = "W") + + Label(self.scale_menu, text="Left Boundary", font= font2).grid(row = 3, column = 0, sticky = "E") #Entry for lower height + self.x_lower_crop = Scale(self.scale_menu, from_=0, to=self.img_width, orient = HORIZONTAL, length=400, command= self.scale_update) + self.x_lower_crop.set(0) + self.x_lower_crop.grid(row = 3, column = 1, sticky = "W") + + Label(self.scale_menu, text="Right Boundary", font= font2).grid(row = 4, column = 0, sticky = "E") #Entry for upper height + self.x_upper_crop = Scale(self.scale_menu, from_=0, to=self.img_width, orient = HORIZONTAL, length=400, command= self.scale_update) + self.x_upper_crop.set(self.img_width) + self.x_upper_crop.grid(row = 4, column = 1, sticky = "W") + + scale_btn = Button(self.scale_menu, text="Done Cropping", bg="purple", font=font2, command = self.scale_select) + scale_btn.grid(row = 6, columnspan = 2) + + def set_scale_manual_entry(self): ##Reads from an entry box to override the automatic or to avoid using the manual scale reading + if float(self.scale_manual_entry.get()) != 0: + self.scale_ratio = float(self.scale_manual_entry.get()) + self.scale_status.configure(text="The scale ratio has been determined by manual entry.\nContinue to crop the image for thresholds") + self.scale_status.update() + def scale_reader_redo(self): ##If the previous image you worked with is not in the same dimmensions as the new image you can redo the automatic scale reading + ratio = scale_reader(self.path) + if ratio == "Error": + self.scale_status.configure(text="Scale ratio was not determined automatically. Using previous ratio.\nYou can override using an alternative method. Adjust crop if necessary") + self.scale_status.update() + else: + self.scale_status.configure(text="The scale ratio has been re-determined automatically!\nYou can override using buttons. Adjust crop if necessary") + self.scale_status.update() + self.scale_ratio = ratio + + def scale_update(self,x): ##Reads the values of the crop sliders to help measure the scale bar. Updates the image + self.scale_lower_crop_val = int(float(self.img_height)-float(self.y_lower_crop.get())) + self.scale_upper_crop_val = int(float(self.img_height)-float(self.y_upper_crop.get())) + self.scale_left_val = int(self.x_lower_crop.get()) + self.scale_right_val = int(self.x_upper_crop.get()) + + scale_crop = self.orig_img.copy() + scale_crop = scale_crop[self.scale_upper_crop_val:self.scale_lower_crop_val, self.scale_left_val:self.scale_right_val] + scale_resize = resize(scale_crop, int((75/self.image_ratio))) + resize_width = scale_resize.shape[1] + resize_height = scale_resize.shape[0] + self.scale_canvas.config(width=resize_width, height = resize_height) + self.scale_resize = ImageTk.PhotoImage(Image.fromarray(scale_resize)) + crop = self.orig_img[self.scale_upper_crop_val:self.scale_lower_crop_val, 0:self.img_width] + + self.scale_canvas.itemconfigure(self.scale_crop_img, image=self.scale_resize) + self.scale_canvas.update() - orig_resize = resize(orig_scale, img_class.fullsize) #Prepares and adds an image to the GUI - resize_width = orig_resize.shape[1] - resize_height = orig_resize.shape[0] - orig_resize = ImageTk.PhotoImage(Image.fromarray(orig_resize)) - crop_canvas = Canvas(crop_menu, width=resize_width, height=resize_height) - crop_canvas.grid(row = 5, columnspan = 2) - cropping_image= crop_canvas.create_image(0,0, image= orig_resize, anchor=NW) - crop_canvas.update() - - Label(crop_menu, text="Lower Height", font= ("Helvetica", 12)).grid(row = 2, column = 0, sticky = "E") #Entry for lower height - lower_crop = Scale(crop_menu, from_=0, to=orig_img.shape[0], orient = HORIZONTAL, length=400, command= crop_update) - lower_crop.set(0) - lower_crop.grid(row = 2, column = 1, sticky = "W") - - Label(crop_menu, text="Upper Height", font= ("Helvetica", 12)).grid(row = 1, column = 0, sticky = "E") #Entry for upper height - upper_crop = Scale(crop_menu, from_=0, to=orig_img.shape[0], orient = HORIZONTAL, length=400, command= crop_update) - upper_crop.set(orig_img.shape[0]) - upper_crop.grid(row = 1, column = 1, sticky = "W") - - ratio = scale_reader(path) - if ratio == "Error": - scale_reader_btn = Button(crop_menu, text="Scale Reader", bg="lawn green", font=("Helvetica", 12), command = lambda: scale_reader_manual(path, resize_width)) - scale_reader_btn.grid(row =4, column = 0, sticky="E") - else: - Label(crop_menu, text="The scale ratio has been detemined!", font=("Helvetica", 12)).grid(row = 4, column = 1) - scale_ratio = ratio - -def scale_reader_manual(path, menu_size): - global scale_menu, scale_crop_img, scale_canvas, y_lower_crop, y_upper_crop, x_lower_crop, x_upper_crop - if scale_menu is not None: - scale_menu.destroy() - scale_menu = None - scale_menu = Toplevel() - scale_menu.title=("Scale Cropping Menu") - scale_menu.configure(bg="gray69") - scale_menu.minsize(menu_size, 300) - - Label(scale_menu, text=("Adjust the height until the scale bar is isolated"), bg="thistle1", font=("Helvetica", 12)).grid(row = 0, columnspan = 2) - - orig_scale = orig_img.copy() #Creates a scale on the side of the image to help with cropping - orig_resize = resize(orig_scale, img_class.fullsize) #Prepares and adds an image to the GUI - resize_width = orig_resize.shape[1] - resize_height = orig_resize.shape[0] - orig_resize = ImageTk.PhotoImage(Image.fromarray(orig_resize)) - scale_canvas = Canvas(scale_menu, width=resize_width, height=resize_height) - scale_canvas.grid(row = 5, column = 1, columnspan = 2) - scale_crop_img = scale_canvas.create_image(0,0, image= orig_resize, anchor=NW) - scale_canvas.update() - + def scale_select(self): ##Creates a menu to select between two threhsolds to help the computer identify the scale bar + if "scale_select_menu" in dir(self): + self.scale_select_menu.destroy() + self.scale_menu.withdraw() + self.scale_select_menu = Toplevel() + self.scale_select_menu.title("Scale Selection") + self.scale_select_menu.configure(bg="gray69") + + scale_image = self.orig_img.copy() + scale_image = scale_image[self.scale_upper_crop_val:self.scale_lower_crop_val, self.scale_left_val:self.scale_right_val] + + thresh_img_1 = cv2.inRange(scale_image, 200, 255) + thresh_img_2 = cv2.inRange(scale_image, 0, 50) + set_scale_img = ImageTk.PhotoImage(Image.fromarray(thresh_img_1)) + + thresh_img_1_resize = resize(thresh_img_1, 75) + thresh_img_1_resize = ImageTk.PhotoImage(Image.fromarray(thresh_img_1_resize)) + thresh_img_2_resize = resize(thresh_img_2, 75) + thresh_img_2_resize = ImageTk.PhotoImage(Image.fromarray(thresh_img_2_resize)) + + setting_1_btn = Button(self.scale_select_menu, image=thresh_img_1_resize, command = lambda: self.set_scale_threshold(thresh_img_1)) + setting_1_btn.image = thresh_img_1_resize + setting_1_btn.grid(row =2, column = 0) + setting_2_btn = Button(self.scale_select_menu, image=thresh_img_2_resize, command = lambda: self.set_scale_threshold(thresh_img_2)) + setting_2_btn.image = thresh_img_2_resize + setting_2_btn.grid(row = 3, column = 0) + + self.scale_select_canvas = Canvas(self.scale_select_menu, width = scale_image.shape[1], height = scale_image.shape[0]) + self.scale_select_canvas.grid(row = 4, columnspan = 2) + self.scale_select_canvas.update() + self.scale_select_canvas.bind("