def initialize_widgets(self): # Watchers # Update captured count self.count_str = StringVar() self.count_number = IntVar() self.count_number.trace("w", self.update_count_label) # Status message self.status_var = StringVar() self.status_message = Label(self, textvariable=self.status_var, font=self.controller.header_font, relief=GROOVE, padx=10, pady=10) self.status_message.grid(row=0, column=0, sticky=EW, padx=40, pady=20) # Table headers top_headers = ["Sensor #", "Current\nreading", "Last\ncaptured\nvalue", "Last\ndeviation"] self.table_headers = VerticalTable(self, rows=1, columns=len(top_headers)) self.table_headers.update_cells(top_headers) self.table_headers.grid(row=1, column=0, sticky=S) # column indexes self.live_column = 0 self.captured_column = 1 self.deviation_column = 2 # Sensor Data: current readings, last captured, and deviation info sensor_headers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Sensor Z"] self.table = HorizontalTable(self, rows=len(sensor_headers), columns=3, header_values=sensor_headers) self.table.grid(row=2, column=0, rowspan=2, sticky=N) # calibrate button self.calibrate_button = GreenButton(self, text="Calibrate Sensors", command=self.calibrate) self.calibrate_button.grid(row=0, column=1, pady=20) # captured count self.captured_count = Label(self, textvariable=self.count_str, font=self.controller.bold_font) self.captured_count.grid(row=2, column=1, sticky=S, pady=10) # capture button self.capture_button = YellowButton(self, text="Capture Measurements", command=self.capture) self.capture_button.grid(row=3, column=1, sticky=N) # view results button self.results_button = GreenButton(self, text="View Results", command=self.view_results, image=self.controller.arrow_right, compound=RIGHT) self.results_button.grid(row=4, column=1, sticky=SE, padx=20, pady=20) make_rows_responsive(self) make_columns_responsive(self)
def initialize_widgets(self): # Watchers # on image path change self.image_path = StringVar() self.image_path.trace("w", self.on_image_path_change) # responsive image container self.placeholder_image = Image.open("assets/placeholder_image.png") self.responsive_image = ResponsiveImage(self, self.placeholder_image) self.responsive_image.grid(row=0, column=0, rowspan=4) # choose image button self.choose_button = GreenButton(self, text="Choose an image", command=self.load_image) self.choose_button.grid(row=0, column=1, sticky=S) # selected image path self.path_entry = Entry(self, textvariable=self.image_path, state="readonly") self.path_entry.grid(row=1, column=1, sticky=EW, padx=20) # status message in row 2 self.message_var = StringVar() self.message = Label(self, textvariable=self.message_var, font=self.controller.header_font) # begin button self.begin_button = YellowButton(self, text="BEGIN", command=self.begin, image=self.controller.arrow_right, compound=RIGHT) self.begin_button.grid(row=3, column=1, sticky=SE, padx=20, pady=20) # Update widgets self.on_image_path_change() # visited flag self.visit_counter = 0 make_rows_responsive(self) make_columns_responsive(self)
def initialize_widgets(self): # Result image row=0, col=0, columnspan=2 # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=2, column=0, sticky=E, padx=10, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=2, column=1, sticky=W, padx=10, pady=20) # min size of buttons row self.grid_rowconfigure(2, minsize=80) make_rows_responsive(self, ignored=[0]) make_columns_responsive(self)
def initialize_widgets(self): # Empty message self.empty_message = Label( self, text="Nothing to see here. Go capture some measurements!", font=self.controller.header_font) # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=1, column=0, sticky=SE, padx=10, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=1, column=1, sticky=SW, padx=10, pady=20) make_rows_responsive(self) make_columns_responsive(self)
def initialize_widgets(self): # a canvas with scrollbars; the results table goes in it h_scrollbar = AutoScrollbar(self, orient=HORIZONTAL) h_scrollbar.grid(row=1, column=0, columnspan=2, sticky=EW) v_scrollbar = AutoScrollbar(self) v_scrollbar.grid(row=0, column=2, sticky=NS) self.canvas = Canvas(self, xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set) h_scrollbar.config(command=self.canvas.xview) v_scrollbar.config(command=self.canvas.yview) # Empty message self.empty_message = Label( self, text="Nothing to see here. Go capture some measurements!", font=self.controller.header_font) # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=2, column=0, sticky=S, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=2, column=1, sticky=S, pady=20) # responsive except the scrollbars and the buttons make_rows_responsive(self, ignored=[1, 2]) make_columns_responsive(self, ignored=[2])
class ResultsBSC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Slice Results" self.responsive_image = None self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) def initialize_widgets(self): # Result image row=0, col=0, columnspan=2 # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=2, column=0, sticky=E, padx=10, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=2, column=1, sticky=W, padx=10, pady=20) # min size of buttons row self.grid_rowconfigure(2, minsize=80) make_rows_responsive(self, ignored=[0]) make_columns_responsive(self) def on_show_frame(self, event=None): # generate polar coordinates and avg diameter of circumferences data_circumferences = circumferences_to_polar_and_avg_diameter() # create plot figure = Figure(figsize=(5,5), dpi=100) ax = figure.add_subplot(111, projection="polar") # ax.set_title("Circumferences' polar coordinates") # plot both circumferences for (r, theta, _) in data_circumferences: ax.plot(theta, r) # Create a Tk canvas of the plot self.polar_plot = FigureCanvasTkAgg(figure, self) self.polar_plot.show() self.polar_plot.get_tk_widget().grid(row=1, column=1, sticky=NSEW, padx=20) # Show some controls for the figure self.toolbar_container = Frame(self) self.plot_toolbar = NavigationToolbar(self.polar_plot, self.toolbar_container) self.plot_toolbar.update() self.toolbar_container.grid(row=0, column=1, sticky=NSEW, padx=20, pady=20) # original image with both circumferences outlined self.image = get_slice_roi() self.responsive_image = ResponsiveImage(self, self.image) self.responsive_image.grid(row=1, column=0, sticky=NSEW, padx=20, pady=20) def save(self): date = datetime.now().strftime('%Y-%m-%d_%H%M%S') save_path = filedialog.asksaveasfilename(title="Save as", defaultextension=".txt", initialfile="BSC_" + date) # make sure the user didn't cancel the dialog if len(save_path) > 0: if generate_text_file(save_path): # all good messagebox.showinfo("Success!", "File was generated successfully.") # reset BSC self.controller.reset_BSC() # go to home screen self.controller.show_frame("Home") else: messagebox.showerror("Error generating text file", "Make sure you have access to the selected destination.") def discard(self): result = messagebox.askokcancel("Discard results?", "All progress will be lost.", default="cancel", icon="warning") if result: # reset BSC self.controller.reset_BSC() # go to home screen self.controller.show_frame("Home") def reset(self): # destroy the image container if self.responsive_image is not None: self.responsive_image.destroy() self.responsive_image = None self.image = None # destroy the plot self.polar_plot = None
class PickCircumferencesBSC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Select the slice's circumferences" self.circumferences = [] self.selected_circumferences = [] self.responsive_image = None self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) def initialize_widgets(self): # Watchers # Update state of navigation buttons self.current_circumference_var = IntVar() self.current_circumference_var.trace("w", self.update_navigation) # Update count label self.selected_count_var = IntVar() self.selected_count_var.trace("w", self.update_count_text) # Update buttons self.selected_count_text = StringVar() self.selected_count_text.trace("w", self.update_buttons) # title of circumference image self.circumference_title_var = StringVar() self.circumference_title = Label( self, textvariable=self.circumference_title_var, font=self.controller.header_font) self.circumference_title.grid(row=0, column=0, sticky=W, padx=20, pady=20) # Controls to navigate contours self.prev_button = GreenButton( self, text="Previous", image=self.controller.arrow_left, compound=LEFT, command=lambda: self.show_circumference( self.current_circumference_var.get() - 1)) self.prev_button.grid(row=0, column=1, sticky=SE, padx=5, pady=20) self.next_button = GreenButton( self, text="Next", image=self.controller.arrow_right, compound=RIGHT, command=lambda: self.show_circumference( self.current_circumference_var.get() + 1)) self.next_button.grid(row=0, column=2, sticky=SW, pady=20) # image in row=1, col=0, colspan=3, rowspan=4 # instructions instructions_text = "More than 2 circumferences were found.\n" instructions_text += "Select the inner and outer circumference of the bamboo slice." self.instructions = Label(self, text=instructions_text, relief=GROOVE, padx=10, pady=10) self.instructions.grid(row=1, column=3, padx=40) # remove button self.remove_button = RedButton(self, text="Deselect circumference", command=self.deselect) self.remove_button.grid(row=2, column=3) # select button self.select_button = YellowButton(self, text="Use this circumference", command=self.select) self.select_button.grid(row=2, column=3) # Selection count self.selected_count = Label(self, textvariable=self.selected_count_text, font=self.controller.important_font) self.selected_count.grid(row=3, column=3, sticky=N) # confirm button self.confirm_button = YellowButton(self, text="CONFIRM", command=self.confirm, state=DISABLED, cursor="arrow") self.confirm_button.grid(row=4, column=3, sticky=SE, padx=20, pady=20) make_columns_responsive(self, ignored=[1, 2]) make_rows_responsive(self, ignored=[0]) def on_show_frame(self, event=None): # fetch all circumferences self.circumferences = render_all_circumferences() # initialize if it's empty if not self.selected_circumferences: # generate selected flags self.selected_circumferences = [False] * len(self.circumferences) # Show the last object we were browsing, or the 1st one if this is a fresh session try: self.show_circumference(self.current_circumference_var.get()) # In case something weird happens except IndexError: print("could not show circumference; resetting") self.reset() self.on_show_frame() def show_circumference(self, index): self.current_circumference_var.set(index) image = self.circumferences[index] if self.responsive_image is not None: self.responsive_image.destroy() self.responsive_image = ResponsiveImage(self, image) self.responsive_image.grid(row=1, column=0, rowspan=4, columnspan=3, sticky=NSEW, pady=20) def update_navigation(self, *args): current = self.current_circumference_var.get() self.circumference_title_var.set("Circumference #" + str(current + 1)) # toggle prev if current == 0: self.prev_button.configure(state=DISABLED, cursor="arrow") else: self.prev_button.configure(state=NORMAL, cursor="hand2") # toggle next if current == len(self.circumferences) - 1: self.next_button.configure(state=DISABLED, cursor="arrow") else: self.next_button.configure(state=NORMAL, cursor="hand2") # Toggle select and remove buttons self.update_select_remove_buttons() def update_buttons(self, *args): self.update_select_remove_buttons() self.update_confirm_button() def update_select_remove_buttons(self): # only if array has been initialized if len(self.selected_circumferences): # selected if self.selected_circumferences[ self.current_circumference_var.get()]: # this one is selected; show remove button self.select_button.grid_remove() self.remove_button.grid() # not selected else: # show select button self.remove_button.grid_remove() self.select_button.grid() # 2 already selected; disable select button if self.selected_count_var.get() == 2: self.select_button.configure(state=DISABLED, cursor="arrow") else: # still can select; re-enable button self.select_button.configure(state=NORMAL, cursor="hand2") def update_confirm_button(self): # Toggle confirm button if self.selected_count_var.get() == 2: self.confirm_button.configure(state=NORMAL, cursor="hand2") else: self.confirm_button.configure(state=DISABLED, cursor="arrow") def update_count_text(self, *args): # update label text self.selected_count_text.set( str(self.selected_count_var.get()) + " of 2 circumferences selected") def deselect(self): # deselect self.selected_circumferences[ self.current_circumference_var.get()] = False # Decrease count self.selected_count_var.set(self.selected_count_var.get() - 1) def select(self): # mark as selected self.selected_circumferences[ self.current_circumference_var.get()] = True # Increase count self.selected_count_var.set(self.selected_count_var.get() + 1) def confirm(self): selected = [] for index, selected_flag in enumerate(self.selected_circumferences): if selected_flag: selected.append(index) # Apply in backend set_final_circumferences(selected) # Show results self.controller.show_frame("RefObjectBSC") def reset(self): # destroy the image container if self.responsive_image is not None: self.responsive_image.destroy() self.responsive_image = None # clear circumferences self.circumferences.clear() self.selected_circumferences.clear() self.selected_count_var.set(0) # Start showing 1st circumference self.current_circumference_var.set(0)
def initialize_widgets(self): # Watchers # Update state of navigation buttons self.current_circumference_var = IntVar() self.current_circumference_var.trace("w", self.update_navigation) # Update count label self.selected_count_var = IntVar() self.selected_count_var.trace("w", self.update_count_text) # Update buttons self.selected_count_text = StringVar() self.selected_count_text.trace("w", self.update_buttons) # title of circumference image self.circumference_title_var = StringVar() self.circumference_title = Label( self, textvariable=self.circumference_title_var, font=self.controller.header_font) self.circumference_title.grid(row=0, column=0, sticky=W, padx=20, pady=20) # Controls to navigate contours self.prev_button = GreenButton( self, text="Previous", image=self.controller.arrow_left, compound=LEFT, command=lambda: self.show_circumference( self.current_circumference_var.get() - 1)) self.prev_button.grid(row=0, column=1, sticky=SE, padx=5, pady=20) self.next_button = GreenButton( self, text="Next", image=self.controller.arrow_right, compound=RIGHT, command=lambda: self.show_circumference( self.current_circumference_var.get() + 1)) self.next_button.grid(row=0, column=2, sticky=SW, pady=20) # image in row=1, col=0, colspan=3, rowspan=4 # instructions instructions_text = "More than 2 circumferences were found.\n" instructions_text += "Select the inner and outer circumference of the bamboo slice." self.instructions = Label(self, text=instructions_text, relief=GROOVE, padx=10, pady=10) self.instructions.grid(row=1, column=3, padx=40) # remove button self.remove_button = RedButton(self, text="Deselect circumference", command=self.deselect) self.remove_button.grid(row=2, column=3) # select button self.select_button = YellowButton(self, text="Use this circumference", command=self.select) self.select_button.grid(row=2, column=3) # Selection count self.selected_count = Label(self, textvariable=self.selected_count_text, font=self.controller.important_font) self.selected_count.grid(row=3, column=3, sticky=N) # confirm button self.confirm_button = YellowButton(self, text="CONFIRM", command=self.confirm, state=DISABLED, cursor="arrow") self.confirm_button.grid(row=4, column=3, sticky=SE, padx=20, pady=20) make_columns_responsive(self, ignored=[1, 2]) make_rows_responsive(self, ignored=[0])
class ConfigBPC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Configuration" self.initialize_widgets() def initialize_widgets(self): # CALIBRATION # Ring diameter entry value self.ring_diameter_var = StringVar() self.ring_diameter_var.trace("w", self.update_begin_button) # Calibration object value self.calibration_object_var = StringVar() self.calibration_object_var.trace("w", self.update_begin_button) # Distance to end of rail value self.distance_z_var = StringVar() self.distance_z_var.trace("w", self.update_begin_button) # %d = Type of action (1=insert, 0=delete, -1 for others) # %P = value of the entry if the edit is allowed # %S = the text string being inserted or deleted, if any validate_cmd = (self.register(self.validate_dimension), '%d', '%P', '%S') # Calibration settings group self.calibration_settings = LabelFrame( self, text="Calibration Settings", fg="grey", padx=20, pady=20, font=self.controller.header_font) self.calibration_settings.grid(row=0, column=0, sticky=NS + W, padx=20, pady=20) # Ring diameter self.ring_diameter_label = Label(self.calibration_settings, text="Ring structure diameter", anchor=SW, font=self.controller.bold_font) self.ring_diameter_label.grid(row=0, column=0, sticky=SW, padx=20) self.ring_diameter_entry = EntryWithPlaceholder( self.calibration_settings, text_var=self.ring_diameter_var, placeholder_text="0.00", validatecommand=validate_cmd, validate="key", textvariable=self.ring_diameter_var) self.ring_diameter_entry.grid(row=1, column=0, sticky=NW, padx=20, pady=20) # Calibration object self.calibration_object_label = Label(self.calibration_settings, text="Calibration object radius", font=self.controller.bold_font, anchor=SW) self.calibration_object_label.grid(row=0, column=1, sticky=SW, padx=20) self.calibration_object_entry = EntryWithPlaceholder( self.calibration_settings, text_var=self.calibration_object_var, placeholder_text="0.00", validatecommand=validate_cmd, textvariable=self.calibration_object_var, validate="key") self.calibration_object_entry.grid(row=1, column=1, sticky=NW, padx=20, pady=20) # Distance to flat surface at end of rail self.distance_z_label = Label(self.calibration_settings, text="Distance to the end of the rail", anchor=SW, font=self.controller.bold_font) self.distance_z_label.grid(row=2, column=0, sticky=SW, padx=20) self.distance_z_entry = EntryWithPlaceholder( self.calibration_settings, text_var=self.distance_z_var, placeholder_text="0.00", validate="key", validatecommand=validate_cmd, textvariable=self.distance_z_var) self.distance_z_entry.grid(row=3, column=0, sticky=NW, padx=20, pady=20) # Invalid dimension message self.invalid_dimension = Label( self.calibration_settings, text="Dimension must be between 1 and 28 centimeters", fg="red", anchor=W) # make calibration section responsive make_columns_responsive(self.calibration_settings) make_rows_responsive(self.calibration_settings) # description label self.description_label = Label( self, text="Information about the sample (optional)", font=self.controller.bold_font) self.description_label.grid(row=1, column=0, sticky=SW, padx=20, pady=20) # Description text area self.text_area = ScrollableTextArea(self) self.text_area.grid(row=2, column=0, sticky=NW, padx=20) # begin button self.begin_button = YellowButton(self, text="BEGIN", command=self.begin, image=self.controller.arrow_right, compound=RIGHT) self.begin_button.grid(row=3, column=0, sticky=SE, padx=20, pady=20) # set placeholders self.ring_diameter_entry.set_placeholder() self.calibration_object_entry.set_placeholder() self.distance_z_entry.set_placeholder() make_rows_responsive(self) make_columns_responsive(self) def validate_dimension(self, action, value_if_allowed, text): # only when inserting if (action == "1"): if text in "0123456789.": try: # maximum set by size of scanner (8.5 x 11 inches) if float(value_if_allowed) >= 1.0 and float( value_if_allowed) <= 28.0: # remove invalid message self.invalid_dimension.grid_forget() return True else: # Make system bell sound self.bell() # Show invalid message self.invalid_dimension.grid(row=7, column=3, sticky=NSEW, padx=60, pady=20) return False except ValueError: self.bell() return False else: self.bell() return False else: return True def update_begin_button(self, *args): ring_diameter = self.ring_diameter_var.get() calibration_obj = self.calibration_object_var.get() distance_z = self.distance_z_var.get() try: ring_diameter_ok = ring_diameter and float(ring_diameter) >= 1.0 calibration_object_ok = calibration_obj and float( calibration_obj) >= 1.0 distance_z_ok = distance_z and float(distance_z) >= 1.0 # All entries filled if ring_diameter_ok and calibration_object_ok and distance_z_ok: self.begin_button.configure(state=NORMAL, cursor="hand2") else: self.begin_button.configure(state=DISABLED, cursor="arrow") except ValueError: print("cannot cast values to float") self.begin_button.configure(state=DISABLED, cursor="arrow") def begin(self): # save sample description set_sampleDescription(self.text_area.get_text()) # save calibration settings ring_diameter = float(self.ring_diameter_var.get()) calibration_obj = float(self.calibration_object_var.get()) distance_z = float(self.distance_z_var.get()) set_calibration_settings(ringDiameter=ring_diameter, obj_radius=calibration_obj, distance_z=distance_z) # Show sensors live feed self.controller.show_frame("MeasureBPC") def reset(self): # reset description self.text_area.clear_text()
class ConfigBPC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Configuration" self.initialize_widgets() def initialize_widgets(self): # CALIBRATION # Ring diameter entry value self.ring_diameter_var = StringVar() self.ring_diameter_var.trace("w", self.update_begin_button) # Calibration object value self.calibration_object_var = StringVar() self.calibration_object_var.trace("w", self.update_begin_button) # Distance to end of rail value self.distance_z_var = StringVar() self.distance_z_var.trace("w", self.update_begin_button) # %d = Type of action (1=insert, 0=delete, -1 for others) # %P = value of the entry if the edit is allowed # %S = the text string being inserted or deleted, if any # %W = the name of the widget validate_cmd = (self.register(self.validate_calibration_settings), '%d', '%P', '%S', '%W') # Calibration settings group self.calibration_settings = LabelFrame(self, text="Calibration Settings (all measures in centimeters)", fg="grey", padx=20, pady=20, font=self.controller.header_font) self.calibration_settings.grid(row=0, column=0, sticky=NSEW, padx=20, pady=20) # Ring diameter self.ring_diameter_label = Label(self.calibration_settings, text="Ring structure diameter", anchor=SW, font=self.controller.bold_font) self.ring_diameter_label.grid(row=0, column=0, sticky=SW, padx=20) # ring diameter range self.range_ring_diameter = Label(self.calibration_settings, text="[Valid range: 10 - 30]", fg="grey", anchor=SW, font=self.controller.small_font) self.range_ring_diameter.grid(row=1, column=0, sticky=NW, padx=20) self.ring_diameter_entry = EntryWithPlaceholder(self.calibration_settings, text_var=self.ring_diameter_var, placeholder_text="0.00", validatecommand=validate_cmd, validate="key", textvariable=self.ring_diameter_var, name="ring") self.ring_diameter_entry.grid(row=2, column=0, sticky=NW, padx=20, pady=20) # Calibration object self.calibration_object_label = Label(self.calibration_settings, text="Calibration object diameter", font=self.controller.bold_font, anchor=SW) self.calibration_object_label.grid(row=0, column=1, sticky=SW, padx=20) # calibration object diameter range self.range_calibration_obj_diameter = Label(self.calibration_settings, text="[Valid range: 2 - 26]", fg="grey", anchor=NW, font=self.controller.small_font) self.range_calibration_obj_diameter.grid(row=1, column=1, sticky=NW, padx=20) self.calibration_object_entry = EntryWithPlaceholder(self.calibration_settings, text_var=self.calibration_object_var, placeholder_text="0.00", validatecommand=validate_cmd, textvariable=self.calibration_object_var, validate="key", name="calibration_obj") self.calibration_object_entry.grid(row=2, column=1, sticky=NW, padx=20, pady=20) # Distance to flat surface at end of rail self.distance_z_label = Label(self.calibration_settings, text="Distance to the end of the rail", anchor=SW, font = self.controller.bold_font) self.distance_z_label.grid(row=3, column=0, sticky=SW, padx=20) # z distance range self.range_z_distance = Label(self.calibration_settings, text="[Valid range: 15.24 - 645]", fg="grey", anchor=NW, font=self.controller.small_font) self.range_z_distance.grid(row=4, column=0, sticky=NW, padx=20) self.distance_z_entry = EntryWithPlaceholder(self.calibration_settings, text_var=self.distance_z_var, placeholder_text="0.00", validate="key", name="z_distance", validatecommand=validate_cmd, textvariable=self.distance_z_var) self.distance_z_entry.grid(row=5, column=0, sticky=NW, padx=20, pady=20) # calibration object diameter greater than ring diameter self.calibration_obj_greater_ring = Label(self.calibration_settings, text="The calibration object's diameter\ncan't be greater than the ring diameter", fg="red", anchor=NW) # make calibration section responsive make_columns_responsive(self.calibration_settings) make_rows_responsive(self.calibration_settings) # description label self.description_label = Label(self, text="Information about the bamboo sample (optional)", font=self.controller.bold_font) self.description_label.grid(row=1, column=0, sticky=SW, padx=20, pady=20) # Description text area self.text_area = ScrollableTextArea(self) self.text_area.grid(row=2, column=0, sticky=NW, padx=20) # begin button self.begin_button = YellowButton(self, text="BEGIN", command=self.begin, image=self.controller.arrow_right, compound=RIGHT) self.begin_button.grid(row=3, column=0, sticky=SE, padx=20, pady=20) # set placeholders self.ring_diameter_entry.set_placeholder() self.calibration_object_entry.set_placeholder() self.distance_z_entry.set_placeholder() make_rows_responsive(self) make_columns_responsive(self) # min size of buttons row self.grid_rowconfigure(3, minsize=80) def validate_calibration_settings(self, action, value_if_allowed, text, widget): # only when inserting if action == "1": if text in "0123456789.": try: # name of the widget widget_name = str(widget).split(".")[-1] # ring if widget_name == "ring": # valid range [10,30] if float(value_if_allowed) >= 1.0 and float(value_if_allowed) <= 30.0: return True else: # Make system bell sound self.bell() return False # calibration object elif widget_name == "calibration_obj": # valid range [2,26] if float(value_if_allowed) >= 1.0 and float(value_if_allowed) <= 26.0: return True else: # Make system bell sound self.bell() return False # z distance entry else: # valid range [15.24, 645] if float(value_if_allowed) >= 1.0 and float(value_if_allowed) <= 645.0: return True else: # Make system bell sound self.bell() return False except ValueError: self.bell() return False else: self.bell() return False else: return True def update_begin_button(self, *args): ring_diameter = self.ring_diameter_var.get() calibration_obj = self.calibration_object_var.get() distance_z = self.distance_z_var.get() # valid range [10,30] ring_diameter_ok = ring_diameter and float(ring_diameter) >= 10.0 # valid range [2,26] calibration_object_ok = calibration_obj and float(calibration_obj) >= 2.0 # valid range [15.24, 645] distance_z_ok = distance_z and float(distance_z) >= 15.24 # show invalid message when calibration object diameter is greater than ring if ring_diameter_ok and calibration_object_ok and (float(ring_diameter) - float(calibration_obj)) > 0: self.calibration_obj_greater_ring.grid_forget() elif ring_diameter_ok and calibration_object_ok and (float(ring_diameter) - float(calibration_obj)) <= 0: self.calibration_obj_greater_ring.grid(row=3, column=1, sticky=NW, padx=20) # All entries valid if ring_diameter_ok and calibration_object_ok and distance_z_ok and \ (float(ring_diameter) - float(calibration_obj)) > 0: # enable begin button self.begin_button.configure(state=NORMAL, cursor="hand2") # at least one entry is not valid else: # disable begin button self.begin_button.configure(state=DISABLED, cursor="arrow") def begin(self): # save sample description set_sampleDescription(self.text_area.text.get(1.0, END)) # save calibration settings ring_diameter = float(self.ring_diameter_var.get()) calibration_obj = float(self.calibration_object_var.get()) distance_z = float(self.distance_z_var.get()) set_calibration_settings(ringDiameter=ring_diameter, obj_diameter=calibration_obj, distance_z=distance_z) # Show sensors live feed self.controller.show_frame("MeasureBPC") def reset(self): # reset description self.text_area.text.delete(1.0, END)
class MeasureBPC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Live Sensor Readings (cm)" self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) self.bind("<<LeaveFrame>>", self.on_leave_frame) # Queue where live feed thread writes data self.queue = queue.LifoQueue() def add_to_queue(self, data): self.queue.put(data) def initialize_widgets(self): # Watchers # Update captured count self.count_str = StringVar() self.count_number = IntVar() self.count_number.trace("w", self.update_count_label) # Status message self.status_var = StringVar() self.status_message = Label(self, textvariable=self.status_var, font=self.controller.header_font, relief=GROOVE, padx=10, pady=10) self.status_message.grid(row=0, column=0, sticky=EW, padx=40, pady=20) # Table headers top_headers = ["Sensor #", "Current\nreading", "Last\ncaptured\nvalue", "Last\ndeviation"] self.table_headers = VerticalTable(self, rows=1, columns=len(top_headers)) self.table_headers.update_cells(top_headers) self.table_headers.grid(row=1, column=0, sticky=S) # column indexes self.live_column = 0 self.captured_column = 1 self.deviation_column = 2 # Sensor Data: current readings, last captured, and deviation info sensor_headers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "Sensor Z"] self.table = HorizontalTable(self, rows=len(sensor_headers), columns=3, header_values=sensor_headers) self.table.grid(row=2, column=0, rowspan=2, sticky=N) # calibrate button self.calibrate_button = GreenButton(self, text="Calibrate Sensors", command=self.calibrate) self.calibrate_button.grid(row=0, column=1, pady=20) # captured count self.captured_count = Label(self, textvariable=self.count_str, font=self.controller.bold_font) self.captured_count.grid(row=2, column=1, sticky=S, pady=10) # capture button self.capture_button = YellowButton(self, text="Capture Measurements", command=self.capture) self.capture_button.grid(row=3, column=1, sticky=N) # view results button self.results_button = GreenButton(self, text="View Results", command=self.view_results, image=self.controller.arrow_right, compound=RIGHT) self.results_button.grid(row=4, column=1, sticky=SE, padx=20, pady=20) make_rows_responsive(self) make_columns_responsive(self) def on_show_frame(self, event=None): # TODO get count from function self.count_number.set(len(saved_measurement)) # clear live readings from table self.table.clear_column(self.live_column) # Controls update callback self.do_update = True # Flag to only update buttons and message when necessary self.busy_message_set = False # Open port and start reading self.run_live_thread() # start live GUI self.update_live_gui() def run_live_thread(self): self.live_thread = LiveFeedThread(widget=self) self.live_thread.start() def update_live_gui(self): # No Arduino found alert if no_arduino.is_set(): no_arduino.clear() result = messagebox.askretrycancel("Error opening serial port", "Make sure the Arduino is properly connected, and try again.", icon="error") # Retry if result: # open new thread self.run_live_thread() else: self.controller.show_frame("ConfigBPC") return # Arduino disconnected alert if disconnected.is_set(): disconnected.clear() result = messagebox.askretrycancel("Error reading from Arduino", "Make sure the Arduino is properly connected, and try again.", icon="error") # Retry if result: # open new thread self.run_live_thread() else: self.controller.show_frame("ConfigBPC") return # Capturing data if self.live_thread.capture_now.is_set(): # only set message once if not self.busy_message_set: # Show status message self.status_var.set("Capturing data...") # Disable buttons self.disable_buttons() # message has been set self.busy_message_set = True # when data has been captured if self.live_thread.capture_done.is_set(): last_captured = saved_measurement[len(saved_measurement) - 1] # update table with new data self.table.update_column(self.captured_column, last_captured) # clear flags self.live_thread.capture_now.clear() self.live_thread.capture_done.clear() # update captured count label self.count_number.set(self.count_number.get() + 1) self.update_idletasks() # Calibrating sensors elif self.live_thread.calibrate_now.is_set(): # only set message once if not self.busy_message_set: # Show status message self.status_var.set("Calibrating sensors...") # Disable buttons self.disable_buttons() # message has been set self.busy_message_set = True # when calibration is done if self.live_thread.calibration_done.is_set(): deviations = [] # exclude the last one; it's the ultrasonic for i in range(len(sensorArray) - 1): # IR sensor deviation angle deviations.append(sensorArray[i].devAngle) # ultrasonic sensor ultrasonic = sensorArray[len(sensorArray) - 1] deviations.append(ultrasonic.factor) # update table with new data self.table.update_column(self.deviation_column, deviations) # clear flags self.live_thread.calibrate_now.clear() self.live_thread.calibration_done.clear() self.update_idletasks() # Update live feed elif self.live_thread.reading_sensors.is_set() and not self.live_thread.capture_now.is_set()\ and not self.live_thread.calibrate_now.is_set(): try: while True: sensor_readings = self.queue.get_nowait() # Update table with new sensor data self.table.update_column(self.live_column, sensor_readings) # only set message once if self.busy_message_set: # Ready to capture or calibrate self.status_var.set("Ready!") # Restore buttons self.restore_buttons() # no longer busy self.busy_message_set = False self.update_idletasks() except queue.Empty: pass # Sensors are still initializing elif not self.live_thread.reading_sensors.is_set() and self.do_update and not self.busy_message_set: # Show loading message self.status_var.set("Connecting to sensors...") # Disable buttons self.disable_buttons() # message has been set self.busy_message_set = True # Keep updating until we leave this frame if self.do_update: self.after(100, self.update_live_gui) def restore_buttons(self): self.calibrate_button.configure(state=NORMAL, cursor="hand2") self.capture_button.configure(state=NORMAL, cursor="hand2") # only enable view results if there are any if self.count_number.get(): self.results_button.configure(state=NORMAL, cursor="hand2") # leave it disabled, but with a different cursor else: self.results_button.configure(state=DISABLED, cursor="arrow") def disable_buttons(self): self.calibrate_button.configure(state=DISABLED, cursor="wait") self.capture_button.configure(state=DISABLED, cursor="wait") self.results_button.configure(state=DISABLED, cursor="wait") def on_leave_frame(self, event=None): # kill thread and close serial port self.live_thread.kill_thread.set() self.do_update = False def calibrate(self): # only one at a time if not self.live_thread.calibrate_now.is_set(): # clear old values from table self.table.clear_column(self.deviation_column) # Let the worker thread handle it self.live_thread.calibrate_now.set() def capture(self): # only one at a time if not self.live_thread.capture_now.is_set(): # clear old values from table self.table.clear_column(self.captured_column) # Let the worker thread handle it self.live_thread.capture_now.set() def update_count_label(self, *args): self.count_str.set(str(self.count_number.get()) + " measurements captured") def view_results(self): self.controller.show_frame("ResultsBPC") def reset(self): # reset captured count self.count_number.set(0) # clear table self.table.clear_cells()
class ResultsBPC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Review Your Measurements" self.captured_data = [] # TODO Generate headers ? self.sensor_headers = [ "Z (cm)", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "S11", "S12" ] self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) self.bind("<<LeaveFrame>>", self.on_leave_frame) def initialize_widgets(self): # Empty message self.empty_message = Label( self, text="Nothing to see here. Go capture some measurements!", font=self.controller.header_font) # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=1, column=0, sticky=SE, padx=10, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=1, column=1, sticky=SW, padx=10, pady=20) make_rows_responsive(self) make_columns_responsive(self) def on_show_frame(self, event=None): # Enable save button self.save_button.configure(state=NORMAL, cursor="hand2") print("original", saved_measurement) # sort captured measurements by Z value sort_ByZeta(saved_measurement) print("sorted", saved_measurement) # Generate captured measurements table self.create_table() def create_table(self): # Create table if there are any captured measurements if saved_measurement: # Make a copy of the captured measurements self.captured_data = copy.deepcopy(saved_measurement) # Place Z as first element for column in self.captured_data: column.insert(0, column.pop()) self.table = HorizontalTable(self, rows=len(self.captured_data[0]), columns=len(self.captured_data), header_values=self.sensor_headers, can_select_columns=True, button_command=self.delete_z) # Set background of top row for column in range(len(self.captured_data)): self.table.cells[0][column].configure( bg="#5E5E5E", fg="#FFFFFF", font=self.controller.bold_font) self.table.headers[0].configure(bg="#5E5E5E", fg="#FFFFFF") self.table.grid(row=0, column=0, columnspan=2) # load cells with captured measurements self.table.update_cells(self.captured_data) # No captured measurements else: # Show an empty message self.empty_message.grid(row=0, columnspan=2) # disable save button self.save_button.configure(state=DISABLED, cursor="arrow") def destroy_table(self): # Remove checkboxes and table from grid, and destroy them self.table.grid_forget() self.table.destroy() def delete_z(self): # get indices to be deleted deleted_columns = self.table.get_checked_indices() # delete them delete_measurement(deleted_columns) # Re-create table self.destroy_table() self.create_table() def on_leave_frame(self, event=None): # Destroy table if there are any captured measurements if saved_measurement: # Destroy captured measurements table self.destroy_table() # Otherwise hide empty message else: self.empty_message.grid_forget() def save(self): date = datetime.now().strftime('%Y-%m-%d_%H%M%S') save_path = filedialog.asksaveasfilename(title="Save as", defaultextension=".txt", initialfile="BPC_" + date) # make sure the user didn't cancel the dialog if len(save_path) > 0: if generate_textfile(save_path): # all good messagebox.showinfo("Success!", "File was generated successfully.") # reset BPC self.controller.reset_BPC() # go to home screen self.controller.show_frame("Home") else: messagebox.showerror( "Error generating text file", "Make sure you have access to the selected destination.") def discard(self): result = messagebox.askokcancel( "Discard captured measurements?", "You will lose all the measurements you have captured so far.", default="cancel", icon="warning") if result: # reset BPC self.controller.reset_BPC() # go to home screen self.controller.show_frame("Home") def reset(self): self.captured_data.clear()
class ConfigBSC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Select a bamboo slice image" self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) def initialize_widgets(self): # Watchers # on image path change self.image_path = StringVar() self.image_path.trace("w", self.on_image_path_change) # responsive image container self.placeholder_image = Image.open("assets/placeholder_image.png") self.responsive_image = ResponsiveImage(self, self.placeholder_image) self.responsive_image.grid(row=0, column=0, rowspan=4) # choose image button self.choose_button = GreenButton(self, text="Choose an image", command=self.load_image) self.choose_button.grid(row=0, column=1, sticky=S) # selected image path self.path_entry = Entry(self, textvariable=self.image_path, state="readonly") self.path_entry.grid(row=1, column=1, sticky=EW, padx=20) # status message in row 2 self.message_var = StringVar() self.message = Label(self, textvariable=self.message_var, font=self.controller.header_font) # begin button self.begin_button = YellowButton(self, text="BEGIN", command=self.begin, image=self.controller.arrow_right, compound=RIGHT) self.begin_button.grid(row=3, column=1, sticky=SE, padx=20, pady=20) # Update widgets self.on_image_path_change() # visited flag self.visit_counter = 0 make_rows_responsive(self) make_columns_responsive(self) def load_image(self): # open a file chooser dialog and allow the user to select a source image temp_path = filedialog.askopenfilename( title="Select an image to process", filetypes=(("JPG", "*.jpg"), ("JPEG", "*.jpeg"), ("PNG", "*.png"), ("TIF", "*.tif"), ("TIFF", "*.tiff"), ("Windows bitmaps", "*.bmp"))) # ensure a file path was selected if len(temp_path) > 0: # disable button before processing self.begin_button.configure(state=DISABLED, cursor="wait") # Process image self.circumferences_found = process_image(temp_path) # update image path, message, and begin button self.image_path.set(temp_path) # image not found if self.circumferences_found is None: # show error message messagebox.showerror( "Could not process image", "The image may have been moved or renamed, or you may not have access to it." ) else: # Not a fresh session if self.visit_counter > 1: # reset the BSC GUI except this frame self.controller.reset_BSC_GUI(ignored=[type(self)]) # make this the 1st visit self.visit_counter = 1 # user image with all detected circumferences outlined image = get_config_image() # make it responsive self.responsive_image.destroy() self.responsive_image = ResponsiveImage(self, image) self.responsive_image.grid(row=0, column=0, rowspan=4, sticky=NSEW, pady=20) def on_image_path_change(self, *args): # image is selected if self.image_path.get(): # show image path self.path_entry.grid() self.choose_button.configure(text="Change image") # error processing image or none found if self.circumferences_found is None or self.circumferences_found == 0: # disable begin button self.begin_button.configure(state=DISABLED, cursor="arrow") # make message red self.message.configure(fg="red") # error processing image if self.circumferences_found is None: self.message_var.set("Error processing selected image") # show placeholder image self.set_placeholder_image() # none found else: self.message_var.set( str(self.circumferences_found) + " circumference(s) found.\n Choose another image.") # some where found else: # enable begin button self.begin_button.configure(state=NORMAL, cursor="hand2") # make message green self.message.configure(fg="#35AD35") # 1 or 2 found if self.circumferences_found <= 2: self.message_var.set( "Bamboo slice detected!\n No need to choose circumferences." ) # more than 2 found else: self.message_var.set( str(self.circumferences_found) + " circumferences found.\n You must choose two of them." ) # show the message self.message.grid(row=2, column=1, padx=20) # not selected else: self.begin_button.configure(state=DISABLED, cursor="arrow") self.path_entry.grid_remove() self.choose_button.configure(text="Choose an image") # hide the message self.message.grid_remove() def begin(self): # Go to pick circumferences if found more than 2 if get_number_original_circumferences() > 2: self.controller.show_frame("PickCircumferencesBSC") # Go to configure scale else: self.controller.show_frame("RefObjectBSC") def on_show_frame(self, event=None): self.visit_counter += 1 def set_placeholder_image(self): self.responsive_image.destroy() self.responsive_image = ResponsiveImage(self, self.placeholder_image) self.responsive_image.grid(row=0, column=0, rowspan=4) def reset(self): # reset to placeholder image self.set_placeholder_image() # Clear image path self.image_path.set("") # visited flag self.visit_counter = 0
class ResultsBPC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Review saved measurements" self.captured_data = [] # TODO Generate headers ? self.sensor_headers = [ "Z (cm)", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "S11", "S12" ] self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) self.bind("<<LeaveFrame>>", self.on_leave_frame) def initialize_widgets(self): # a canvas with scrollbars; the results table goes in it h_scrollbar = AutoScrollbar(self, orient=HORIZONTAL) h_scrollbar.grid(row=1, column=0, columnspan=2, sticky=EW) v_scrollbar = AutoScrollbar(self) v_scrollbar.grid(row=0, column=2, sticky=NS) self.canvas = Canvas(self, xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set) h_scrollbar.config(command=self.canvas.xview) v_scrollbar.config(command=self.canvas.yview) # Empty message self.empty_message = Label( self, text="Nothing to see here. Go capture some measurements!", font=self.controller.header_font) # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=2, column=0, sticky=S, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=2, column=1, sticky=S, pady=20) # responsive except the scrollbars and the buttons make_rows_responsive(self, ignored=[1, 2]) make_columns_responsive(self, ignored=[2]) def on_show_frame(self, event=None): # restore canvas in grid self.canvas.grid(row=0, column=0, sticky=NSEW, columnspan=2, pady=20) # Enable save button self.save_button.configure(state=NORMAL, cursor="hand2") # sort captured measurements by Z value sort_ByZeta(saved_measurement) # Generate captured measurements table self.create_table() def create_table(self): # Create table if there are any captured measurements if saved_measurement: # Make a copy of the captured measurements self.captured_data = copy.deepcopy(saved_measurement) # Place Z as first element for column in self.captured_data: column.insert(0, column.pop()) # the results table self.table = HorizontalTable(self.canvas, rows=len(self.captured_data[0]), columns=len(self.captured_data), header_values=self.sensor_headers, can_select_columns=True, button_command=self.delete_z) # Set background of top row for column in range(len(self.captured_data)): self.table.cells[0][column].configure( bg="#5E5E5E", fg="#FFFFFF", font=self.controller.bold_font) self.table.headers[0].configure(bg="#5E5E5E", fg="#FFFFFF") # load cells with captured measurements self.table.update_cells(self.captured_data) # place the table inside the canvas self.canvas.create_window(0, 0, anchor=NW, window=self.table) # wait for the canvas to create table self.table.update_idletasks() # update scroll region of table self.canvas.config(scrollregion=self.canvas.bbox("all")) # No captured measurements else: # Hide canvas self.canvas.grid_forget() # Show an empty message self.empty_message.grid(row=0, columnspan=2) # disable save button self.save_button.configure(state=DISABLED, cursor="arrow") def destroy_table(self): # Remove table from canvas self.canvas.delete("all") # Destroy the table self.table.destroy() def delete_z(self): # get indices to be deleted deleted_columns = self.table.get_checked_indices() # delete them delete_measurement(deleted_columns) # Re-create table self.destroy_table() self.create_table() def on_leave_frame(self, event=None): # Destroy table if there are any captured measurements if saved_measurement: self.destroy_table() # Otherwise hide empty message else: self.empty_message.grid_forget() def save(self): date = datetime.now().strftime('%Y-%m-%d_%H%M%S') save_path = filedialog.asksaveasfilename(title="Save as", defaultextension=".txt", initialfile="BPC_" + date) # make sure the user didn't cancel the dialog if len(save_path) > 0: if generate_text_file(save_path): # ask to open text file should_open_file = messagebox.askyesno( "Open generated text file?", "Your measurements have been saved in " + save_path + "\n\nWould you like to open the text file now?") # open the text file if should_open_file: try: Popen(save_path, shell=True) except OSError as e: print("Error opening text file:", e) else: messagebox.showerror( "Error generating text file", "Make sure you have access to the selected destination.") def discard(self): result = messagebox.askokcancel( "Discard captured measurements?", "You will lose all the measurements you have captured so far.", default="cancel", icon="warning") if result: # reset BPC self.controller.reset_BPC() # go to home screen self.controller.show_frame("Home") def reset(self): self.captured_data.clear()
def initialize_widgets(self): # WATCHERS # Update state of navigation buttons self.current_contour_var = IntVar() self.current_contour_var.trace("w", self.update_navigation) # Transition between stages self.selected_object_var = StringVar() self.selected_object_var.trace("w", self.update_stage) # Switch between horizontal and vertical bisections self.dimension_type = StringVar() self.dimension_type.set("horizontal") self.dimension_type.trace("w", self.update_image) # Update confirm button state self.real_dimension_var = StringVar() self.real_dimension_var.trace("w", self.update_confirm_button) # Min size of object title column self.grid_columnconfigure(0, minsize=150) # Min size of change button column self.grid_columnconfigure(1, minsize=100) # title of ref object image self.ref_object_var = StringVar() self.ref_object_title = Label(self, textvariable=self.ref_object_var, font=self.controller.header_font) self.ref_object_title.grid(row=0, column=0, sticky=W, padx=20, pady=20) # STAGE 1 # Controls to navigate contours self.prev_button = GreenButton(self, text="Previous", image=self.controller.arrow_left, compound=LEFT, command=lambda: self.show_contour( self.current_contour_var.get() - 1)) self.stage1_widgets.append( (self.prev_button, lambda: self.prev_button.grid( row=0, column=1, sticky=SE, padx=5, pady=20))) self.next_button = GreenButton(self, text="Next", image=self.controller.arrow_right, compound=RIGHT, command=lambda: self.show_contour( self.current_contour_var.get() + 1)) self.stage1_widgets.append( (self.next_button, lambda: self.next_button.grid(row=0, column=2, sticky=SW, pady=20) )) # image in row=1, col=0, colspan=3, rowspan varies with stage # instructions self.instructions_var = StringVar() self.instructions = Label(self, textvariable=self.instructions_var, relief=GROOVE, padx=10, pady=10) self.instructions.grid(row=1, column=3, padx=40) # select reference object self.select_button = YellowButton( self, text="Use this object as reference", command=lambda: self.selected_object_var.set( self.current_contour_var.get())) self.stage1_widgets.append( (self.select_button, lambda: self.select_button.grid(row=2, column=3, sticky=N))) # STAGE 2 # Change reference object self.change_button = GreenButton( self, text="Change", command=lambda: self.selected_object_var.set("")) self.stage2_widgets.append( (self.change_button, lambda: self.change_button.grid( row=0, column=1, columnspan=2, sticky=SE, pady=20))) # select dimension to enter for the reference object self.dimension_label = Label(self, text="Select dimension", font=self.controller.header_font, anchor=SW) self.stage2_widgets.append( (self.dimension_label, lambda: self.dimension_label.grid( row=2, column=3, sticky=NSEW, padx=40, pady=10))) self.dimension_width = Radiobutton(self, text="Width", variable=self.dimension_type, value="horizontal", cursor="hand2", anchor=SW) self.dimension_height = Radiobutton(self, text="Height", variable=self.dimension_type, value="vertical", cursor="hand2", anchor=NW) self.stage2_widgets.append( (self.dimension_width, lambda: self.dimension_width.grid( row=3, column=3, sticky=NSEW, padx=60))) self.stage2_widgets.append( (self.dimension_height, lambda: self.dimension_height.grid( row=4, column=3, sticky=NSEW, padx=60))) # user-entered dimension (for pixels-per-metric) self.real_dimension_label = Label( self, text="True measure of pink line (in centimeters)", font=self.controller.header_font, anchor=SW) self.stage2_widgets.append( (self.real_dimension_label, lambda: self.real_dimension_label.grid( row=5, column=3, padx=40, pady=20, sticky=NSEW))) # %d = Type of action (1=insert, 0=delete, -1 for others) # %P = value of the entry if the edit is allowed # %S = the text string being inserted or deleted, if any validate_cmd = (self.register(self.validate_dimension), '%d', '%P', '%S') self.real_dimension = EntryWithPlaceholder( self, text_var=self.real_dimension_var, placeholder_text="0.00", validate="key", validatecommand=validate_cmd, textvariable=self.real_dimension_var) self.stage2_widgets.append( (self.real_dimension, lambda: self.real_dimension.grid( row=6, column=3, padx=60, sticky=NW))) # Invalid dimension message self.invalid_dimension = Label( self, text="Dimension must be between 1 and 28 centimeters", fg="red", anchor=W) # confirm button self.confirm_button = YellowButton(self, text="CONFIRM", command=self.confirm) self.stage2_widgets.append( (self.confirm_button, lambda: self.confirm_button.grid( row=8, column=3, sticky=SE, padx=20, pady=20))) # set dimension entry placeholder self.real_dimension.set_placeholder() # start in stage 1 self.selected_object_var.set("")
class RefObjectBSC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Configure the image's scale" self.responsive_image = None self.boxes = [] self.box = None self.stage1_widgets = [] self.stage2_widgets = [] self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) def initialize_widgets(self): # WATCHERS # Update state of navigation buttons self.current_contour_var = IntVar() self.current_contour_var.trace("w", self.update_navigation) # Transition between stages self.selected_object_var = StringVar() self.selected_object_var.trace("w", self.update_stage) # Switch between horizontal and vertical bisections self.dimension_type = StringVar() self.dimension_type.set("horizontal") self.dimension_type.trace("w", self.update_image) # Update confirm button state self.real_dimension_var = StringVar() self.real_dimension_var.trace("w", self.update_confirm_button) # Min size of object title column self.grid_columnconfigure(0, minsize=150) # Min size of change button column self.grid_columnconfigure(1, minsize=100) # title of ref object image self.ref_object_var = StringVar() self.ref_object_title = Label(self, textvariable=self.ref_object_var, font=self.controller.header_font) self.ref_object_title.grid(row=0, column=0, sticky=W, padx=20, pady=20) # STAGE 1 # Controls to navigate contours self.prev_button = GreenButton(self, text="Previous", image=self.controller.arrow_left, compound=LEFT, command=lambda: self.show_contour( self.current_contour_var.get() - 1)) self.stage1_widgets.append( (self.prev_button, lambda: self.prev_button.grid( row=0, column=1, sticky=SE, padx=5, pady=20))) self.next_button = GreenButton(self, text="Next", image=self.controller.arrow_right, compound=RIGHT, command=lambda: self.show_contour( self.current_contour_var.get() + 1)) self.stage1_widgets.append( (self.next_button, lambda: self.next_button.grid(row=0, column=2, sticky=SW, pady=20) )) # image in row=1, col=0, colspan=3, rowspan varies with stage # instructions self.instructions_var = StringVar() self.instructions = Label(self, textvariable=self.instructions_var, relief=GROOVE, padx=10, pady=10) self.instructions.grid(row=1, column=3, padx=40) # select reference object self.select_button = YellowButton( self, text="Use this object as reference", command=lambda: self.selected_object_var.set( self.current_contour_var.get())) self.stage1_widgets.append( (self.select_button, lambda: self.select_button.grid(row=2, column=3, sticky=N))) # STAGE 2 # Change reference object self.change_button = GreenButton( self, text="Change", command=lambda: self.selected_object_var.set("")) self.stage2_widgets.append( (self.change_button, lambda: self.change_button.grid( row=0, column=1, columnspan=2, sticky=SE, pady=20))) # select dimension to enter for the reference object self.dimension_label = Label(self, text="Select dimension", font=self.controller.header_font, anchor=SW) self.stage2_widgets.append( (self.dimension_label, lambda: self.dimension_label.grid( row=2, column=3, sticky=NSEW, padx=40, pady=10))) self.dimension_width = Radiobutton(self, text="Width", variable=self.dimension_type, value="horizontal", cursor="hand2", anchor=SW) self.dimension_height = Radiobutton(self, text="Height", variable=self.dimension_type, value="vertical", cursor="hand2", anchor=NW) self.stage2_widgets.append( (self.dimension_width, lambda: self.dimension_width.grid( row=3, column=3, sticky=NSEW, padx=60))) self.stage2_widgets.append( (self.dimension_height, lambda: self.dimension_height.grid( row=4, column=3, sticky=NSEW, padx=60))) # user-entered dimension (for pixels-per-metric) self.real_dimension_label = Label( self, text="True measure of pink line (in centimeters)", font=self.controller.header_font, anchor=SW) self.stage2_widgets.append( (self.real_dimension_label, lambda: self.real_dimension_label.grid( row=5, column=3, padx=40, pady=20, sticky=NSEW))) # %d = Type of action (1=insert, 0=delete, -1 for others) # %P = value of the entry if the edit is allowed # %S = the text string being inserted or deleted, if any validate_cmd = (self.register(self.validate_dimension), '%d', '%P', '%S') self.real_dimension = EntryWithPlaceholder( self, text_var=self.real_dimension_var, placeholder_text="0.00", validate="key", validatecommand=validate_cmd, textvariable=self.real_dimension_var) self.stage2_widgets.append( (self.real_dimension, lambda: self.real_dimension.grid( row=6, column=3, padx=60, sticky=NW))) # Invalid dimension message self.invalid_dimension = Label( self, text="Dimension must be between 1 and 28 centimeters", fg="red", anchor=W) # confirm button self.confirm_button = YellowButton(self, text="CONFIRM", command=self.confirm) self.stage2_widgets.append( (self.confirm_button, lambda: self.confirm_button.grid( row=8, column=3, sticky=SE, padx=20, pady=20))) # set dimension entry placeholder self.real_dimension.set_placeholder() # start in stage 1 self.selected_object_var.set("") def on_show_frame(self, event=None): # fetch all reference objects self.boxes = render_boxes() # Show the last object we were browsing, or the 1st one if this is a fresh session try: self.show_contour(self.current_contour_var.get()) # In case something weird happens except IndexError: print("could not show contour; resetting") self.reset() self.on_show_frame() def show_contour(self, index): self.box = self.boxes[index] self.current_contour_var.set(index) self.update_image() def update_image(self, *args): if self.box is not None: image = self.box[self.dimension_type.get()][0] if self.responsive_image is not None: self.responsive_image.destroy() self.responsive_image = ResponsiveImage(self, image) # stage 2 if self.selected_object_var.get(): self.responsive_image.grid(row=1, column=0, rowspan=8, columnspan=3, sticky=NSEW, pady=20) # stage 1 else: self.responsive_image.grid(row=1, column=0, rowspan=2, columnspan=3, sticky=NSEW, pady=20) def update_navigation(self, *args): current = self.current_contour_var.get() # update image title self.ref_object_var.set("Object #" + str(current + 1)) # toggle prev if current == 0: self.prev_button.configure(state=DISABLED, cursor="arrow") else: self.prev_button.configure(state=NORMAL, cursor="hand2") # toggle next if current == len(self.boxes) - 1: self.next_button.configure(state=DISABLED, cursor="arrow") else: self.next_button.configure(state=NORMAL, cursor="hand2") def update_stage(self, *args): # Object has been selected if self.selected_object_var.get(): # Show stage 2 self.set_stage2() else: # Show stage 1 self.set_stage1() def set_stage1(self): current = self.current_contour_var.get() # Update image title self.ref_object_var.set("Object #" + str(current + 1)) # Update instructions self.instructions_var.set( "Choose an object for which you know its width or height.\n" "This will allow to calculate the real dimensions of the bamboo slice." ) # Hide stage 2 for (widget, grid_command) in self.stage2_widgets: widget.grid_forget() # Hide invalid message self.invalid_dimension.grid_forget() # Show stage 1 for (widget, grid_command) in self.stage1_widgets: grid_command() # Update responsive image if self.responsive_image is not None: self.responsive_image.grid(row=1, column=0, rowspan=2, columnspan=3, sticky=NSEW, pady=20) # Update responsive reset_both_responsive(self) make_columns_responsive(self, ignored=[1, 2]) make_rows_responsive(self, ignored=[0]) def set_stage2(self): # Update image title self.ref_object_var.set("Selected Reference Object") # Update instructions self.instructions_var.set( "Now please tell us how long is the dimension given by the pink line in the image.\n" "Provide the most decimals for more accurate results!") # Hide stage 1 for (widget, grid_command) in self.stage1_widgets: widget.grid_forget() # Show stage 2 for (widget, grid_command) in self.stage2_widgets: grid_command() # Update responsive image if self.responsive_image is not None: self.responsive_image.grid(row=1, column=0, rowspan=8, columnspan=3, sticky=NSEW, pady=20) # Update responsive reset_both_responsive(self) make_columns_responsive(self, ignored=[0, 1]) make_rows_responsive(self, ignored=[0, 2, 3, 4, 5, 6, 7]) def validate_dimension(self, action, value_if_allowed, text): # only when inserting if (action == "1"): if text in "0123456789.": try: # maximum set by size of scanner (8.5 x 11 inches) if float(value_if_allowed) >= 1.0 and float( value_if_allowed) <= 28.0: # remove invalid message self.invalid_dimension.grid_forget() return True else: # Make system bell sound self.bell() # Show invalid message self.invalid_dimension.grid(row=7, column=3, sticky=NSEW, padx=60, pady=20) return False except ValueError: self.bell() return False else: self.bell() return False else: return True def update_confirm_button(self, *args): dimension = self.real_dimension_var.get() try: # not empty string and greater than 1 if dimension and float(dimension) >= 1.0: self.confirm_button.configure(state=NORMAL, cursor="hand2") else: self.confirm_button.configure(state=DISABLED, cursor="arrow") except ValueError: print("cannot cast dimension value to float") self.confirm_button.configure(state=DISABLED, cursor="arrow") def confirm(self): # pixels per metric = distance in pixels / distance in centimeters ppm = self.box[self.dimension_type.get()][1] / float( self.real_dimension.get()) set_pixels_per_metric(ppm) # Show results self.controller.show_frame("ResultsBSC") def reset(self): # destroy the image container if self.responsive_image is not None: self.responsive_image.grid_forget() self.responsive_image.destroy() self.responsive_image = None # reset real dimension entry field self.real_dimension.set_placeholder() # Default to width self.dimension_type.set("horizontal") # clear selected object; start in stage 1 self.selected_object_var.set("") # clear ref objects self.boxes.clear() self.box = None # Start showing 1st contour box self.current_contour_var.set(0)
class ResultsBSC(Frame): def __init__(self, parent, controller): Frame.__init__(self, parent) self.controller = controller self.title = "Slice processing results" self.responsive_image = None self.initialize_widgets() self.bind("<<ShowFrame>>", self.on_show_frame) def initialize_widgets(self): # Result image row=0, col=0, columnspan=2 # Save button self.save_button = YellowButton(self, text="Save coordinates", command=self.save, image=self.controller.save_icon, compound=LEFT) self.save_button.grid(row=2, column=0, sticky=E, padx=10, pady=20) # Discard button self.discard_button = RedButton(self, text="DISCARD", command=self.discard) self.discard_button.grid(row=2, column=1, sticky=W, padx=10, pady=20) # min size of buttons row self.grid_rowconfigure(2, minsize=80) make_rows_responsive(self, ignored=[0]) make_columns_responsive(self) def on_show_frame(self, event=None): final_circumferences = translate_coordinates() # create plot figure = Figure(figsize=(5, 5), dpi=100) ax = figure.add_subplot(111) # colors of plot series (outer = red, inner = blue) colors = ("r", "b") for ((contour_x, contour_y), (centroid_x, centroid_y), avg_diameter), color in zip(final_circumferences, colors): # plot the contours ax.plot(contour_x, contour_y, color=color) # plot the centroids ax.plot(centroid_x, centroid_y, color=color, marker="o") # Create a Tk canvas of the plot self.plot = FigureCanvasTkAgg(figure, self) self.plot.show() self.plot.get_tk_widget().grid(row=1, column=1, sticky=NSEW, padx=20) # Show some controls for the figure self.toolbar_container = Frame(self) self.plot_toolbar = NavigationToolbar2TkAgg(self.plot, self.toolbar_container) self.plot_toolbar.update() self.toolbar_container.grid(row=0, column=1, sticky=NSEW, padx=20, pady=20) # original image with both circumferences outlined self.image = get_slice_roi() self.responsive_image = ResponsiveImage(self, self.image, anchor=CENTER) self.responsive_image.grid(row=1, column=0, sticky=NSEW, padx=20, pady=20) def save(self): date = datetime.now().strftime('%Y-%m-%d_%H%M%S') save_path = filedialog.asksaveasfilename(title="Save as", defaultextension=".txt", initialfile="BSC_" + date) # make sure the user didn't cancel the dialog if len(save_path) > 0: if generate_text_file(save_path): # ask to open text file should_open_file = messagebox.askyesno( "Open generated text file?", "The bamboo slice information has been saved in " + save_path + "\n\nWould you like to open the text file now?") # open the text file if should_open_file: try: Popen(save_path, shell=True) except OSError as e: print("Error opening text file:", e) else: messagebox.showerror( "Error generating text file", "Make sure you have access to the selected destination.") def discard(self): result = messagebox.askokcancel("Discard results?", "All progress will be lost.", default="cancel", icon="warning") if result: # reset BSC self.controller.reset_BSC() # go to home screen self.controller.show_frame("Home") def reset(self): # destroy the image container if self.responsive_image is not None: self.responsive_image.destroy() self.responsive_image = None self.image = None # destroy the plot self.plot = None
def initialize_widgets(self): # CALIBRATION # Ring diameter entry value self.ring_diameter_var = StringVar() self.ring_diameter_var.trace("w", self.update_begin_button) # Calibration object value self.calibration_object_var = StringVar() self.calibration_object_var.trace("w", self.update_begin_button) # Distance to end of rail value self.distance_z_var = StringVar() self.distance_z_var.trace("w", self.update_begin_button) # %d = Type of action (1=insert, 0=delete, -1 for others) # %P = value of the entry if the edit is allowed # %S = the text string being inserted or deleted, if any validate_cmd = (self.register(self.validate_dimension), '%d', '%P', '%S') # Calibration settings group self.calibration_settings = LabelFrame( self, text="Calibration Settings", fg="grey", padx=20, pady=20, font=self.controller.header_font) self.calibration_settings.grid(row=0, column=0, sticky=NS + W, padx=20, pady=20) # Ring diameter self.ring_diameter_label = Label(self.calibration_settings, text="Ring structure diameter", anchor=SW, font=self.controller.bold_font) self.ring_diameter_label.grid(row=0, column=0, sticky=SW, padx=20) self.ring_diameter_entry = EntryWithPlaceholder( self.calibration_settings, text_var=self.ring_diameter_var, placeholder_text="0.00", validatecommand=validate_cmd, validate="key", textvariable=self.ring_diameter_var) self.ring_diameter_entry.grid(row=1, column=0, sticky=NW, padx=20, pady=20) # Calibration object self.calibration_object_label = Label(self.calibration_settings, text="Calibration object radius", font=self.controller.bold_font, anchor=SW) self.calibration_object_label.grid(row=0, column=1, sticky=SW, padx=20) self.calibration_object_entry = EntryWithPlaceholder( self.calibration_settings, text_var=self.calibration_object_var, placeholder_text="0.00", validatecommand=validate_cmd, textvariable=self.calibration_object_var, validate="key") self.calibration_object_entry.grid(row=1, column=1, sticky=NW, padx=20, pady=20) # Distance to flat surface at end of rail self.distance_z_label = Label(self.calibration_settings, text="Distance to the end of the rail", anchor=SW, font=self.controller.bold_font) self.distance_z_label.grid(row=2, column=0, sticky=SW, padx=20) self.distance_z_entry = EntryWithPlaceholder( self.calibration_settings, text_var=self.distance_z_var, placeholder_text="0.00", validate="key", validatecommand=validate_cmd, textvariable=self.distance_z_var) self.distance_z_entry.grid(row=3, column=0, sticky=NW, padx=20, pady=20) # Invalid dimension message self.invalid_dimension = Label( self.calibration_settings, text="Dimension must be between 1 and 28 centimeters", fg="red", anchor=W) # make calibration section responsive make_columns_responsive(self.calibration_settings) make_rows_responsive(self.calibration_settings) # description label self.description_label = Label( self, text="Information about the sample (optional)", font=self.controller.bold_font) self.description_label.grid(row=1, column=0, sticky=SW, padx=20, pady=20) # Description text area self.text_area = ScrollableTextArea(self) self.text_area.grid(row=2, column=0, sticky=NW, padx=20) # begin button self.begin_button = YellowButton(self, text="BEGIN", command=self.begin, image=self.controller.arrow_right, compound=RIGHT) self.begin_button.grid(row=3, column=0, sticky=SE, padx=20, pady=20) # set placeholders self.ring_diameter_entry.set_placeholder() self.calibration_object_entry.set_placeholder() self.distance_z_entry.set_placeholder() make_rows_responsive(self) make_columns_responsive(self)
def initialize_widgets(self): # CALIBRATION # Ring diameter entry value self.ring_diameter_var = StringVar() self.ring_diameter_var.trace("w", self.update_begin_button) # Calibration object value self.calibration_object_var = StringVar() self.calibration_object_var.trace("w", self.update_begin_button) # Distance to end of rail value self.distance_z_var = StringVar() self.distance_z_var.trace("w", self.update_begin_button) # %d = Type of action (1=insert, 0=delete, -1 for others) # %P = value of the entry if the edit is allowed # %S = the text string being inserted or deleted, if any # %W = the name of the widget validate_cmd = (self.register(self.validate_calibration_settings), '%d', '%P', '%S', '%W') # Calibration settings group self.calibration_settings = LabelFrame(self, text="Calibration Settings (all measures in centimeters)", fg="grey", padx=20, pady=20, font=self.controller.header_font) self.calibration_settings.grid(row=0, column=0, sticky=NSEW, padx=20, pady=20) # Ring diameter self.ring_diameter_label = Label(self.calibration_settings, text="Ring structure diameter", anchor=SW, font=self.controller.bold_font) self.ring_diameter_label.grid(row=0, column=0, sticky=SW, padx=20) # ring diameter range self.range_ring_diameter = Label(self.calibration_settings, text="[Valid range: 10 - 30]", fg="grey", anchor=SW, font=self.controller.small_font) self.range_ring_diameter.grid(row=1, column=0, sticky=NW, padx=20) self.ring_diameter_entry = EntryWithPlaceholder(self.calibration_settings, text_var=self.ring_diameter_var, placeholder_text="0.00", validatecommand=validate_cmd, validate="key", textvariable=self.ring_diameter_var, name="ring") self.ring_diameter_entry.grid(row=2, column=0, sticky=NW, padx=20, pady=20) # Calibration object self.calibration_object_label = Label(self.calibration_settings, text="Calibration object diameter", font=self.controller.bold_font, anchor=SW) self.calibration_object_label.grid(row=0, column=1, sticky=SW, padx=20) # calibration object diameter range self.range_calibration_obj_diameter = Label(self.calibration_settings, text="[Valid range: 2 - 26]", fg="grey", anchor=NW, font=self.controller.small_font) self.range_calibration_obj_diameter.grid(row=1, column=1, sticky=NW, padx=20) self.calibration_object_entry = EntryWithPlaceholder(self.calibration_settings, text_var=self.calibration_object_var, placeholder_text="0.00", validatecommand=validate_cmd, textvariable=self.calibration_object_var, validate="key", name="calibration_obj") self.calibration_object_entry.grid(row=2, column=1, sticky=NW, padx=20, pady=20) # Distance to flat surface at end of rail self.distance_z_label = Label(self.calibration_settings, text="Distance to the end of the rail", anchor=SW, font = self.controller.bold_font) self.distance_z_label.grid(row=3, column=0, sticky=SW, padx=20) # z distance range self.range_z_distance = Label(self.calibration_settings, text="[Valid range: 15.24 - 645]", fg="grey", anchor=NW, font=self.controller.small_font) self.range_z_distance.grid(row=4, column=0, sticky=NW, padx=20) self.distance_z_entry = EntryWithPlaceholder(self.calibration_settings, text_var=self.distance_z_var, placeholder_text="0.00", validate="key", name="z_distance", validatecommand=validate_cmd, textvariable=self.distance_z_var) self.distance_z_entry.grid(row=5, column=0, sticky=NW, padx=20, pady=20) # calibration object diameter greater than ring diameter self.calibration_obj_greater_ring = Label(self.calibration_settings, text="The calibration object's diameter\ncan't be greater than the ring diameter", fg="red", anchor=NW) # make calibration section responsive make_columns_responsive(self.calibration_settings) make_rows_responsive(self.calibration_settings) # description label self.description_label = Label(self, text="Information about the bamboo sample (optional)", font=self.controller.bold_font) self.description_label.grid(row=1, column=0, sticky=SW, padx=20, pady=20) # Description text area self.text_area = ScrollableTextArea(self) self.text_area.grid(row=2, column=0, sticky=NW, padx=20) # begin button self.begin_button = YellowButton(self, text="BEGIN", command=self.begin, image=self.controller.arrow_right, compound=RIGHT) self.begin_button.grid(row=3, column=0, sticky=SE, padx=20, pady=20) # set placeholders self.ring_diameter_entry.set_placeholder() self.calibration_object_entry.set_placeholder() self.distance_z_entry.set_placeholder() make_rows_responsive(self) make_columns_responsive(self) # min size of buttons row self.grid_rowconfigure(3, minsize=80)