diff --git a/.gitignore b/.gitignore index 08bd8aa..77b3f90 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ coverage.xml dmypy.json # IDEA config files -.idea/ \ No newline at end of file +.idea/ +.vscode/ diff --git a/coco_viewer.png b/coco_viewer.png new file mode 100644 index 0000000..2736f2e Binary files /dev/null and b/coco_viewer.png differ diff --git a/cocoviewer.py b/cocoviewer.py old mode 100644 new mode 100755 index 470a4be..a904250 --- a/cocoviewer.py +++ b/cocoviewer.py @@ -20,16 +20,12 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") parser = argparse.ArgumentParser(description="View images with bboxes from the COCO dataset") -parser.add_argument("-i", "--images", default="", type=str, metavar="PATH", help="path to images folder") parser.add_argument( - "-a", - "--annotations", - default="", + "annotations", type=str, - metavar="PATH", help="path to annotations json file", ) - +parser.add_argument("imagedir", default="./",nargs='?', type=str, help="path to images folder") class Data: """Handles data related stuff.""" @@ -84,6 +80,9 @@ def next_image(self): def previous_image(self): """Loads the previous image in a list.""" self.current_image = self.images.prev() + + def select_image(self, ind): + self.current_image = self.images.set(ind) def parse_coco(annotations_file: str) -> tuple: @@ -137,7 +136,7 @@ def prepare_colors(n_objects: int, shuffle: bool = True) -> list: def get_categories(instances: dict) -> dict: """Extracts categories from annotations file and prepares color for each one.""" # Parse categories - colors = prepare_colors(n_objects=80, shuffle=True) + colors = prepare_colors(n_objects=len(instances["categories"]), shuffle=True) categories = list( zip( [[category["id"], category["name"]] for category in instances["categories"]], @@ -180,7 +179,9 @@ def draw_bboxes(draw, objects, labels, obj_categories, ignore, width, label_size # TODO: Implement notification message as popup window font = ImageFont.load_default() - tw, th = draw.textsize(text, font) + text_size = draw.textbbox((0, 0), text, font=font) + tw = text_size[2] - text_size[0] + th = text_size[3] - text_size[1] tx0 = b[0] ty0 = b[1] - th @@ -212,7 +213,10 @@ def draw_masks(draw, objects, obj_categories, ignore, alpha): if isinstance(m, list): for m_ in m: if m_: - draw.polygon(m_, outline=fill, fill=fill) + try: + draw.polygon(m_, outline=fill, fill=fill) + except: + print('WARNING draw_masks: invalid polygon', m_) # RLE mask for collection of objects (iscrowd=1) elif isinstance(m, dict) and objects[i]["iscrowd"]: mask = rle_to_mask(m["counts"][:-1], m["size"][0], m["size"][1]) @@ -267,6 +271,12 @@ def prev(self): self.n -= 1 current_image = self.image_list[self.n] return current_image + + def set(self,ind): + assert ind>=0 and ind < self.max + self.n = ind + current_image = self.image_list[self.n] + return current_image class ImagePanel(ttk.Frame): @@ -487,6 +497,20 @@ def __init__(self, parent): self.object_box.pack(side=tk.TOP, fill=tk.Y, expand=True) self.add(self.object_subpanel) +class ImagelistPanel(ttk.PanedWindow): + def __init__(self, parent): + super().__init__(parent) + # Image list subpanel + self.pack(side=tk.LEFT, fill=tk.Y) + self.imglist_subpanel = ttk.Frame() + ttk.Label(self.imglist_subpanel, text="image list", borderwidth=2, background="gray50").pack(side=tk.TOP, fill=tk.X) + # image list controller + scrollbar = tk.Scrollbar(self.imglist_subpanel) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.imglist_box = tk.Listbox(self.imglist_subpanel, selectmode=tk.SINGLE, exportselection=0,yscrollcommand=scrollbar.set) + scrollbar.config(command=self.imglist_box.yview) + self.imglist_box.pack(side=tk.TOP, fill=tk.Y, expand=True) + self.add(self.imglist_subpanel) class SlidersBar(ttk.Frame): def __init__(self, parent): @@ -504,16 +528,18 @@ def __init__(self, parent): # Mask transparency controller self.mask_slider = tk.Scale(self, label="mask", from_=0, to=255, tickinterval=50, orient=tk.HORIZONTAL) self.mask_slider.pack(side=tk.LEFT, fill=tk.X, expand=True) + class Controller: - def __init__(self, data, root, image_panel, statusbar, menu, objects_panel, sliders): + def __init__(self, data, root, image_panel, statusbar, menu, objects_panel, sliders, imglist_panel): self.data = data # data layer self.root = root # root window self.image_panel = image_panel # image panel self.statusbar = statusbar # statusbar on the bottom self.menu = menu # main menu on the top self.objects_panel = objects_panel + self.imglist_panel = imglist_panel self.sliders = sliders # StatusBar Vars @@ -564,9 +590,10 @@ def __init__(self, data, root, image_panel, statusbar, menu, objects_panel, slid self.selected_objs = None self.category_box_content = tk.StringVar() self.object_box_content = tk.StringVar() + self.imglist_box_content = tk.StringVar() self.objects_panel.category_box.configure(listvariable=self.category_box_content) self.objects_panel.object_box.configure(listvariable=self.object_box_content) - + self.imglist_panel.imglist_box.configure(listvariable=self.imglist_box_content) # Sliders Setup self.bbox_thickness = tk.IntVar() self.bbox_thickness.set(3) @@ -587,6 +614,8 @@ def __init__(self, data, root, image_panel, statusbar, menu, objects_panel, slid self.current_img_categories = None self.update_img() + self.update_imglist_box() + def set_locals(self): self.bboxes_on_local = self.bboxes_on_global.get() self.labels_on_local = self.labels_on_global.get() @@ -688,18 +717,20 @@ def exit(self, event=None): self.root.quit() def next_img(self, event=None): - self.data.next_image() - self.set_locals() - self.selected_cats = None - self.selected_objs = None - self.update_img(local=False) + cur_id = self.imglist_panel.imglist_box.index('active') + cur_id = min(self.imglist_panel.imglist_box.size() - 1,cur_id+1) + self.imglist_panel.imglist_box.selection_clear(0, tk.END) + self.imglist_panel.imglist_box.activate(cur_id) + self.imglist_panel.imglist_box.selection_set(cur_id) + self.select_img(None) def prev_img(self, event=None): - self.data.previous_image() - self.set_locals() - self.selected_cats = None - self.selected_objs = None - self.update_img(local=False) + cur_id = self.imglist_panel.imglist_box.index('active') + cur_id = max(0,cur_id-1) + self.imglist_panel.imglist_box.selection_clear(0, tk.END) + self.imglist_panel.imglist_box.activate(cur_id) + self.imglist_panel.imglist_box.selection_set(cur_id) + self.select_img(None) def save_image(self, event=None): """Saves composed image as png file.""" @@ -825,6 +856,21 @@ def select_object(self, event): self.selected_cats = selected_cats self.update_img() + def update_imglist_box(self): + imglist = [f'{i:04d} {name}' for i,name in self.data.images.image_list] + self.imglist_box_content.set(imglist) + max_len = max(len(item) for item in imglist) + self.imglist_panel.imglist_box.config(width = max_len) + self.imglist_panel.imglist_box.select_set(0,tk.END) + + def select_img(self, event): + ind = self.imglist_panel.imglist_box.curselection()[0] + self.data.select_image(ind) + self.set_locals() + self.selected_cats = None + self.selected_objs = None + self.update_img(local=False) + def update_sliders_state(self): self.bbox_slider_status_update() self.label_slider_status_update() @@ -864,6 +910,7 @@ def bind_events(self): # Objects Panel self.objects_panel.category_box.bind("<>", self.select_category) self.objects_panel.object_box.bind("<>", self.select_object) + self.imglist_panel.imglist_box.bind("<>", self.select_img) self.image_panel.bind("", lambda e: self.image_panel.focus_set()) @@ -876,21 +923,18 @@ def main(): args = parser.parse_args() root = tk.Tk() root.title("COCO Viewer") + # Set the window icon + icon_path = os.path.dirname(os.path.realpath(__file__))+'/coco_viewer.png' + root.iconphoto(True, tk.PhotoImage(file=icon_path)) - if not args.images or not args.annotations: - root.geometry("300x150") # app size when no data is provided - messagebox.showwarning("Warning!", "Nothing to show.\nPlease specify a path to the COCO dataset!") - print_info("Exiting...") - root.destroy() - return - - data = Data(args.images, args.annotations) + data = Data(args.imagedir, args.annotations) statusbar = StatusBar(root) sliders = SlidersBar(root) objects_panel = ObjectsPanel(root) + imglist_panel = ImagelistPanel(root) menu = Menu(root) image_panel = ImagePanel(root) - Controller(data, root, image_panel, statusbar, menu, objects_panel, sliders) + Controller(data, root, image_panel, statusbar, menu, objects_panel, sliders, imglist_panel) root.mainloop()