def test_draw_menu(self): tool = AnnotationTool() tool.load_app(True) appMenu = AppMenu(tool) menu, fileMenu, toolMenu, helpMenu = appMenu._draw_menu() self.assertTrue(menu.winfo_exists()) self.assertEqual(len(menu.winfo_children()), 2) self.assertEqual(menu.entrycget(1, 'label'), 'File') self.assertEqual(menu.entrycget(2, 'label'), 'Help') self.assertTrue(fileMenu.winfo_exists()) self.assertEqual(fileMenu.entrycget(1, 'label'), 'New Blank Project') self.assertEqual(fileMenu.entrycget(2, 'label'), 'New Project Wizard') self.assertEqual(fileMenu.entrycget(3, 'label'), 'Open Project') self.assertEqual(fileMenu.entrycget(5, 'label'), 'Quit') self.assertFalse(toolMenu) self.assertTrue(helpMenu.winfo_exists()) self.assertEqual(helpMenu.entrycget(1, 'label'), "OpenAnnotation Documentation") self.assertEqual(helpMenu.entrycget(2, 'label'), "About OpenAnnotation") tool.project_open = True tool.annotations = [] menu, fileMenu, toolMenu, helpMenu = appMenu._draw_menu() self.assertTrue(menu.winfo_exists()) self.assertEqual(len(menu.winfo_children()), 3) self.assertEqual(menu.entrycget(1, 'label'), 'File') self.assertEqual(menu.entrycget(2, 'label'), 'Tools') self.assertEqual(menu.entrycget(3, 'label'), 'Help') self.assertTrue(fileMenu.winfo_exists()) self.assertEqual(fileMenu.entrycget(1, 'label'), 'New Blank Project') self.assertEqual(fileMenu.entrycget(2, 'label'), 'New Project Wizard') self.assertEqual(fileMenu.entrycget(3, 'label'), 'Open Project') self.assertEqual(fileMenu.entrycget(4, 'label'), 'Save Project') self.assertEqual(fileMenu.entrycget(5, 'label'), 'Close Project') self.assertEqual(fileMenu.entrycget(7, 'label'), "Import File(s)") self.assertEqual(fileMenu.entrycget(8, 'label'), "Import Entire Directory") self.assertEqual(fileMenu.entrycget(9, 'label'), "Export Project to CSV") self.assertEqual(fileMenu.entrycget(11, 'label'), 'Quit') self.assertTrue(toolMenu.winfo_exists()) self.assertEqual(toolMenu.entrycget(1, 'label'), "Class Manager") self.assertEqual(toolMenu.entrycget(2, 'label'), 'Options') self.assertEqual(toolMenu.entrycget(3, 'label'), "Options") self.assertTrue(helpMenu.winfo_exists()) self.assertEqual(helpMenu.entrycget(1, 'label'), "OpenAnnotation Documentation") self.assertEqual(helpMenu.entrycget(2, 'label'), "About OpenAnnotation") tool.annotations.append(Annotation()) self.assertTrue(toolMenu.winfo_exists()) _, _, toolMenu, _ = appMenu._draw_menu() self.assertEqual(toolMenu.entrycget(1, 'label'), "Class Manager") self.assertEqual(toolMenu.entrycget(2, 'label'), 'Options') self.assertEqual(toolMenu.entrycget(3, 'label'), "Reset Image") self.assertEqual(toolMenu.entrycget(4, 'label'), 'Select Image #')
class AnnotationTool(object): def __init__(self): ''' This is the main annotation tool object. Running this .py file will open the application. Parameters ---------- None Attributes ---------- file_ext : list (Default = ['.jpg', '.png']) Image file extensions currently supported by OpenAnnotation window_size_strings : list (Default = ["1024x768", "800x600"]) These are the supported Window Sizes. window_size_index : int Which window size in window_size_strings is currently selected window_width : int The number of pixels the window width is window_height : int The number of pixels the window height is page : int The current page in the image Navigator navigator_width : int The number of images in the Image Navigator toolbar_height : int The number of pixels the Toolbar height is canvas_width : int The remaining window size is for the Canvas object canvas_height : int The remaining window size is for the Canvas object project_open : bool This tracks if the project is open for display purposes saved : bool This tracks if the current project is saved app_menu : The OpenAnnotation AppMenu object Initialize the menu across the top of the window top_colors : list The default colors are blue, red, green, cyan, yellow, and magenta Raises ------ None Returns ------- None ''' self.file_ext = ['.jpg', '.png'] self.window_size_strings = ["1024x768", "800x600"] self.window_size_index = 0 self.window_width = 1024 self.window_height = 768 self.page = 0 self.img_per_page = 50 self.toolbar_height = 50 self.navigator_width = 200 self.canvas_width = self.window_width - self.navigator_width self.canvas_height = self.window_height - self.toolbar_height self.project_open = False self.saved = True self.app_menu = AppMenu(self) self.top_colors = ['#0000FF', '#FF0000', '#00FF00', '#00FFFF', '#FF00FF', '#FFFF00'] def load_app(self, test=False): ''' This loads the main window for the OpenAnnotation applications Parameters ---------- test : bool (Default = False) If test=True, then the window is not actually drawn Attributes ---------- window : tkinter Tk object This is the main application window background : tkinter Frame object This is the frame that covers the entire window object Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' # Build Window self.window = tk.Tk() self.window.title("OpenAnnotation 0.2.1") # to define the title self.window.geometry("%dx%d" % (self.window_width,self.window_height)) self.app_menu._draw_menu() self.background = Frame(self.window, width=self.window_width, height=self.window_height) self.background.pack() # Create Load Screen Buttons new_button = Button(self.background, text="New Blank Project", width = 20, height=3, command=self.app_menu._new) new_button.grid(row=0, column=0, sticky='n', pady=2 ) new_wiz_button = Button(self.background, text="New Project Wizard", width = 20, height=3, command=self.app_menu._new_project_wizard) new_wiz_button.grid(row=1, column=0, sticky='n', pady=2 ) load_button = Button(self.background, text="Load Project", width=20, height=3, command=self.app_menu._open) load_button.grid(row=2, column=0, sticky='n', pady=2) quit_button = Button(self.background, text="Quit", width=20, height=3, command=self.app_menu._quit) quit_button.grid(row=3, column=0, sticky='n', pady=2) if not test: self.window.mainloop() return True def _draw_object_class_manager(self): ''' This draws the Object Class Management Tool in a popup window Parameters ---------- None Attributes ---------- obj_mgr : OpenAnnotation ObjectClassManager object This is the Object Class Management tool accessible from the toolbar up top Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' self.obj_mgr = ObjectClassManager(self) return True def _draw_workspace(self): ''' This draws the main project in the background frame Parameters ---------- None Attributes ---------- background : tkinter Frame object This is the frame that covers the entire window object Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' self.app_menu._draw_menu() self.background.destroy() # Build Background Frame self.background = Frame(self.window, bg="gray", width=self.window_width, height=self.window_height) self.background.place(x=0, y=0, width = self.window_width, height = self.window_height) if self.annotations: self.num_pages = int(len(self.annotations)/self.img_per_page) # Draw Toolbar on Left toolbar = Toolbar(self) # Draw Canvas on Right self.draw_canvas() if len(self.annotations): self.navigator = Navigator(self) return True def draw_canvas(self): ''' This draws the canvas in the main window. Parameters ---------- None Attributes ---------- canvas : tkinter Canvas object This is what the image is drawn on aspect_ratio : float Compute the scale factor to shrink/increase the image to fit in the canvas boxes : list A list for the OpenAnnotation InteractiveBox objects Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' # Draw Canvas on Right canvas_frame = Frame(self.background, bg='green', width=self.canvas_width, height=self.canvas_height) canvas_frame.place(x=0, y=self.toolbar_height, width = self.canvas_width, height = self.canvas_height, ) self.canvas = Canvas(canvas_frame, width=self.canvas_width, height=self.canvas_height) self.canvas.place(x=0, y=0, width = self.canvas_width, height = self.canvas_height, ) if len(self.annotations): self.aspect_ratio = max(self.img.size[0]/(self.canvas_width), self.img.size[1]/(self.canvas_height)) new_size = (int(self.img.size[0]/self.aspect_ratio), int(self.img.size[1]/self.aspect_ratio)) if self.window.winfo_ismapped(): pil_img = ImageTk.PhotoImage(self.img.resize(new_size, Image.ANTIALIAS)) else: pil_img = None self.canvas.image = pil_img self.canvas.create_image(0, 0, anchor=tk.NW, image=pil_img) self.boxes = [] for i, roi in enumerate(self.annotations[self.current_file].roi): left, top, right, bottom = roi.getBox() lbl = self.annotations[self.current_file].label[i] left = left / self.aspect_ratio top = top / self.aspect_ratio right = right / self.aspect_ratio bottom = bottom / self.aspect_ratio color = self.colorspace[lbl] box = InteractiveBox(self, left, top, right, bottom, color) box.draw_box(i) self.boxes.append(box) # Only allow bounding boxes to be drawn if they can be tied to a class if len(self.class_list) and len(self.annotations): self.canvas.bind("<Button-1>",self._on_click) self.canvas.bind("<ButtonRelease-1>",self._on_release) self.canvas.bind("<B1-Motion>", self._on_move_press) return True def _on_click(self, event): ''' This handles the click-hold Event Parameters ---------- event : tkinter Event Event that handles the mouse being clicked, creating the first of two bounding box corners Attributes ---------- clicked : tuple The (x,y) coordinate for the mouse click event box_resize_mode : string Defines the type of box action that will occur. Options are 'RIGHT', 'LEFT', 'TOP', 'BOTTOM', or 'NEW'. resize_box_id : int This is which ROI object has been clicked on Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' self.clicked = (event.x, event.y) for i, box in enumerate(self.boxes): if box.right_clicked(self.clicked[0], self.clicked[1]): self.box_resize_mode = 'RIGHT' self.resize_box_id = i return True elif box.left_clicked(self.clicked[0], self.clicked[1]): self.box_resize_mode = 'LEFT' self.resize_box_id = i return True elif box.top_clicked(self.clicked[0], self.clicked[1]): self.box_resize_mode = 'TOP' self.resize_box_id = i return True elif box.bottom_clicked(self.clicked[0], self.clicked[1]): self.box_resize_mode = 'BOTTOM' self.resize_box_id = i return True self.box_resize_mode = 'NEW' return True def _on_release(self, event): ''' This handles when the mouse left-button has been released, which adds a new bounding box or resizes an existing box. Parameters ---------- event : tkinter Event Event that handles the mouse being clicked, creating the first of two bounding box corners Attributes ---------- None Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' if self.box_resize_mode == 'NEW': top = min(self.clicked[1], event.y) bottom = max(self.clicked[1], event.y) left = min(self.clicked[0], event.x) right = max(self.clicked[0], event.x) label = self.class_list.index(self.selected_class.get()) color = self.colorspace[label] box = InteractiveBox(self, left, top, right, bottom, color) self.canvas.delete(self.rect) del self.rect box.draw_box(len(self.annotations[self.current_file].roi)) top = self.aspect_ratio * top bottom = self.aspect_ratio * bottom left = self.aspect_ratio * left right = self.aspect_ratio * right roi = ROI() roi.push(left, top) roi.push(right, bottom) self.annotations[self.current_file].push(roi,label) self.class_count[label] = self.class_count[label] + 1 self.draw_canvas() self.saved = False self.box_resize_mode = 'NEW' return True def _on_move_press(self, event): ''' This handles the event where the mouse left-button is held down and the cursor is moved across the screen. Parameters ---------- event : tkinter Event Event that handles the mouse being clicked, creating the first of two bounding box corners Attributes ---------- rect : tkinter Canvas create_rectangle object The bounding box that will be drawn on the Canvas box_end : tuple The (x,y) coordinate for the mouse motion event Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' if hasattr(self, 'resize_box_id'): box_id = self.resize_box_id if self.box_resize_mode != 'NEW': if hasattr(self.boxes[box_id],'close_button'): self.boxes[box_id].close_button.destroy() if self.box_resize_mode == 'RIGHT': self.canvas.coords(self.boxes[box_id].rect, self.boxes[box_id].left, self.boxes[box_id].top, event.x, self.boxes[box_id].bottom) self.boxes[box_id].right = event.x self.annotations[self.current_file].roi[box_id].points[1][0] =\ event.x * self.aspect_ratio elif self.box_resize_mode == 'LEFT': self.canvas.coords(self.boxes[self.resize_box_id].rect, event.x, self.boxes[self.resize_box_id].top, self.boxes[self.resize_box_id].right, self.boxes[self.resize_box_id].bottom) self.boxes[self.resize_box_id].left = event.x self.annotations[self.current_file].roi[box_id].points[0][0] =\ event.x * self.aspect_ratio elif self.box_resize_mode == 'TOP': self.canvas.coords(self.boxes[self.resize_box_id].rect, self.boxes[self.resize_box_id].left, event.y, self.boxes[self.resize_box_id].right, self.boxes[self.resize_box_id].bottom) self.boxes[self.resize_box_id].top = event.y self.annotations[self.current_file].roi[box_id].points[0][1] =\ event.y * self.aspect_ratio elif self.box_resize_mode == 'BOTTOM': self.canvas.coords(self.boxes[self.resize_box_id].rect, self.boxes[self.resize_box_id].left, self.boxes[self.resize_box_id].top, self.boxes[self.resize_box_id].right, event.y) self.boxes[self.resize_box_id].bottom = event.y self.annotations[self.current_file].roi[box_id].points[1][1] =\ event.y * self.aspect_ratio elif not hasattr(self, 'rect'): label = self.selected_class.get() color = self.colorspace[self.class_list.index(label)] self.rect = self.canvas.create_rectangle(self.clicked[0], self.clicked[1], self.clicked[0], self.clicked[1], width=5, outline=color) else: self.box_end = (event.x, event.y) self.canvas.coords(self.rect, self.clicked[0], self.clicked[1], event.x, event.y) return True def _load_image_from_file(self): ''' This uses PIL to load the current Image displayed in the project Parameters ---------- None Attributes ---------- img : PIL Image object The Image is opened and rotated upright using EXIF metadata Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' if self.window.winfo_ismapped(): self.img = Image.open(self.file_list[self.current_file]) else: self.img = Image.new('RGB', (self.canvas_width, self.canvas_height)) rot = self.annotations[self.current_file].rotation if rot > 0: self.img = self.img.transpose(rot) def _reset_image(self): ''' This deletes all annotations made on a given image Parameters ---------- None Attributes ---------- None Raises ------ None Returns ------- complete : bool Returns True for unittesting ''' for lbl in self.annotations[self.current_file].label: self.class_count[lbl] = self.class_count[lbl] - 1 self.annotations[self.current_file].label = [] self.annotations[self.current_file].roi = [] self._draw_workspace() return True