Example #1
0
class Viewer:
    def __init__(self):
        self.index = None
        self.database = []
        self.setup()

    def setup(self):
        self.root = tk.Tk()
        self.root.title("labelview")
        self.root.state("iconic")
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        # self.root.resizable(width=False, height=False)  # cannot change window size
        self.w = self.root.winfo_screenwidth()
        self.h = self.root.winfo_screenheight()
        
        # configure ttk style
        s = ttk.Style()
        s.configure("File.TButton", foreground="blue")
        s.configure("Flow.TButton", foreground="red")
        s.configure("Save.TButton", foreground="blue")

        self.tabs = ttk.Notebook(self.root)

        # thumbnail tab
        self.i = 0.8  # the fraction of image region, horizontal
        self.f = 0.4  # the fraction of file control region, vertical
        self.c = 0.4  # the fraction of checkbox region, vertical

        self.thumb_tab = ttk.Frame(self.tabs)
        self.tabs.add(self.thumb_tab, text="  thumbnail  ")

        # left side control panel
        self.control = ttk.Panedwindow(self.thumb_tab, orient="vertical")
        self.control.grid(row=0, column=0)

        # file flow control
        self.flowctl = ttk.Labelframe(self.control, text="file flow control", width=self.w*(1-self.i), height=self.h*self.f)
        self.control.add(self.flowctl)


        # open single file
        self.open_f = ttk.Button(self.flowctl, text="open file", command=self.load_file)
        self.open_f.grid(row=0, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)
        # load single csv/xml corresponding to wsi file
        self.load_l = ttk.Button(self.flowctl, text="load csv/xml", command=self.load_labels)
        self.load_l.grid(row=0, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)

        # open directory
        self.open_d = ttk.Button(self.flowctl, text="open dir", style="File.TButton", command=self.load_files)
        self.open_d.grid(row=1, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)
        # set csv/xml directory
        self.load_ld = ttk.Button(self.flowctl, text="load csv/xml dir", style="File.TButton", command=self.load_labels_dir)
        self.load_ld.grid(row=1, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)

        # current directory name
        self.dir_name_ = ttk.Label(self.flowctl, text="dir:")
        self.dir_name_.grid(row=2, column=0, columnspan=1, sticky="ew", ipady=5, padx=10, pady=10)
        self.dir_name = ttk.Label(self.flowctl, text="----")
        self.dir_name.grid(row=2, column=1, columnspan=1, sticky="ew", ipady=5, padx=10, pady=10)
        # display file count
        self.n_count_ = ttk.Label(self.flowctl, text="count:")
        self.n_count_.grid(row=2, column=2, columnspan=1, sticky="ew", ipady=5, padx=10, pady=10)
        self.n_count = ttk.Label(self.flowctl, text="----")
        self.n_count.grid(row=2, column=3, columnspan=1, sticky="ew", ipady=5, padx=10, pady=10)

        # display fname
        self.fname = ttk.Label(self.flowctl, text="XXXX.kfb/.tif")
        self.fname.grid(row=3, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)
        # display lname
        self.lname = ttk.Label(self.flowctl, text="XXXX.csv/.xml")
        self.lname.grid(row=3, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)

        # previous
        self.prev_b = ttk.Button(self.flowctl, text="previous", style="Flow.TButton", command=lambda: self.update(step=-1))
        self.prev_b.grid(row=4, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)
        # next
        self.next_b = ttk.Button(self.flowctl, text="next", style="Flow.TButton", command=lambda: self.update(step=1))
        self.next_b.grid(row=4, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)

        

        # checkbox control panel
        self.colorctl = ttk.Labelframe(self.control, text="choose classes", width=self.w*(1-self.i), height=self.h*self.c)
        self.control.add(self.colorctl)

        # add blur thumbnail image button
        self.blur = ttk.Button(self.colorctl, text="blur", command=lambda: self.update(blur=True))
        self.blur.grid(row=0, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=5)
        # add confirm button
        self.conform = ttk.Button(self.colorctl, text="confirm", command=lambda: self.update())
        self.conform.grid(row=0, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=5)
        # add checkboxes
        self.colorblock = []
        self.checkboxes = []
        for i, class_i in enumerate(cfg.CLASSES):
            var = IntVar(value=1)
            clb = ttk.Label(self.colorctl, text="--", background=cfg.COLOURS[class_i])
            chk = ttk.Checkbutton(self.colorctl, text=class_i, variable=var)
            if i < math.ceil(len(cfg.CLASSES)/2):
                clb.grid(row=1+i, column=0, columnspan=1, sticky="ew", ipady=1, padx=10, pady=2)
                chk.grid(row=1+i, column=1, columnspan=1, sticky="w", ipady=1, padx=10, pady=2)
            else:
                clb.grid(row=1+i-math.ceil(len(cfg.CLASSES)/2), column=2, columnspan=1, sticky="ew", ipady=1, padx=10, pady=2)
                chk.grid(row=1+i-math.ceil(len(cfg.CLASSES)/2), column=3, columnspan=1, sticky="w", ipady=1, padx=10, pady=2)
            self.colorblock.append(clb)
            self.checkboxes.append(var)
        
        
        # separator
        self.separator = ttk.Separator(self.thumb_tab, orient="vertical")
        self.separator.grid(row=0, column=1, sticky="ns")

        # right side image panel
        self.display = tk.Canvas(self.thumb_tab, width=self.w*self.i, height=self.h)
        self.display.grid(row=0, column=2)



        # labeled images tab
        self.w_left_i = 256  # the size of left size panel, horizontal
        self.s_i = 0.2  # the fraction of file info display and label file save control, vertical
        self.i_i = 0.2  # the fraction of images flow control panel, vertical
        self.c_i = 0.4  # the fraction of checkbox control panel, vertical

        self.image_tab = ttk.Frame(self.tabs)
        self.image_tab.bind("<Visibility>", self.on_visibility)  # clean and update contents when switch to this tab
        self.tabs.add(self.image_tab, text=" label images ")

        # left side control panel
        self.control_i = ttk.Panedwindow(self.image_tab, orient="vertical")
        self.control_i.grid(row=0, column=0)


        # file info display and label file save control
        self.savectl_i = ttk.Labelframe(self.control_i, text="label file write control", width=self.w_left_i, height=self.h*self.s_i)
        self.control_i.add(self.savectl_i)

        # set label file save dir
        self.label_dir = ttk.Button(self.savectl_i, text="set label file save dir", command=self.set_save_dir)
        self.label_dir.grid(row=0, column=0, columnspan=4, sticky="ew", ipady=5, padx=10, pady=10)
        # display file progress
        self.n_count_i = ttk.Label(self.savectl_i, text="file progress")
        self.n_count_i.grid(row=1, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)
        # save changes
        self.save_changes = ttk.Button(self.savectl_i, text="save changes", style="Save.TButton", command=self.save_labels)
        self.save_changes.grid(row=1, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)


        # images flow control
        self.flowctl_i = ttk.Labelframe(self.control_i, text="images flow control", width=self.w_left_i, height=self.h*self.i_i)
        self.control_i.add(self.flowctl_i)

        # previous
        self.prev_b_i = ttk.Button(self.flowctl_i, text="previous batch", style="Flow.TButton", command=lambda: self.update_i(step=-1))
        self.prev_b_i.grid(row=0, column=0, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)
        # next
        self.next_b_i = ttk.Button(self.flowctl_i, text="next batch", style="Flow.TButton", command=lambda: self.update_i(step=1))
        self.next_b_i.grid(row=0, column=2, columnspan=2, sticky="ew", ipady=5, padx=10, pady=10)


        # checkbox control
        self.colorctl_i = ttk.Labelframe(self.control_i, text="choose classes", width=self.w_left_i, height=self.h*self.c_i)
        self.control_i.add(self.colorctl_i)

        # images view progress
        self.image_pro = ttk.Label(self.colorctl_i, text="images view progress")
        self.image_pro.grid(row=0, column=0, columnspan=2, sticky="ew", ipady=5, padx=5, pady=10)
        # confirm button
        self.confirm_i = ttk.Button(self.colorctl_i, text="confirm", command=lambda: self.update_i(step=0))
        self.confirm_i.grid(row=0, column=2, columnspan=2, sticky="ew", ipady=5, padx=5, pady=10)
        # set display modes: the number of images in a row
        self.M_ = ttk.Label(self.colorctl_i, text="# M:")
        self.M_.grid(row=1, column=0, columnspan=1, sticky="w", ipady=1, padx=5, pady=5)
        self.M = ttk.Entry(self.colorctl_i)
        self.M.insert(0, "3")
        self.M.config(width=8)
        self.M.grid(row=1, column=1, columnspan=1, sticky="w", ipady=1, padx=5, pady=5)
        # set image size: the times of image size over label box
        self.N_ = ttk.Label(self.colorctl_i, text="# N:")
        self.N_.grid(row=1, column=2, columnspan=1, sticky="w", ipady=1, padx=5, pady=5)
        self.N = ttk.Entry(self.colorctl_i)
        self.N.insert(0, "2")
        self.N.config(width=8)
        self.N.grid(row=1, column=3, columnspan=1, sticky="w", ipady=1, padx=5, pady=5)
        # add checkboxes
        self.colorblock_i = []
        self.checkboxes_i = []
        for i,class_i in enumerate(cfg.CLASSES):
            var = IntVar(value=0)
            clb = ttk.Label(self.colorctl_i, text="--", background='#ffffff')
            chk = ttk.Checkbutton(self.colorctl_i, text=class_i, variable=var)
            if i < math.ceil(len(cfg.CLASSES)/2):
                clb.grid(row=2+i, column=0, columnspan=1, sticky="ew", ipady=1, padx=5, pady=2)
                chk.grid(row=2+i, column=1, columnspan=1, sticky="w", ipady=1, padx=5, pady=2)
            else:
                clb.grid(row=2+i-math.ceil(len(cfg.CLASSES)/2), column=2, columnspan=1, sticky="ew", ipady=1, padx=5, pady=2)
                chk.grid(row=2+i-math.ceil(len(cfg.CLASSES)/2), column=3, columnspan=1, sticky="w", ipady=1, padx=5, pady=2)
            self.colorblock_i.append(clb)
            self.checkboxes_i.append(var)


        # thumbnail display for comparison purpose
        self.thumbmini = tk.Canvas(self.control_i, width=self.w_left_i, height=self.w_left_i)
        self.control_i.add(self.thumbmini)


        # separator
        self.separator_i = ttk.Separator(self.image_tab, orient="vertical")
        self.separator_i.grid(row=0, column=1, sticky="ns")

        # right side image panel
        self.display_i = tk.Canvas(self.image_tab, width=self.w-self.w_left_i, height=self.h)
        # self.display_i.bind("<d>", self.on_d_pressed)
        # self.display_i.focus_set()
        self.display_i.grid(row=0, column=2)


        # third tab: logging
        self.log_tab = ttk.Frame(self.tabs)
        self.logger = Logger(self.log_tab)
        self.tabs.add(self.log_tab, text="     log     ", padding=5)

        self.tabs.pack()


    def load_file(self):
        """ open wsi file dialog """
        fname = filedialog.askopenfilename(filetypes=(("*.kfb files", "*.kfb"), ("*.tif files", "*.tif")))
        if not fname:
            messagebox.showinfo("warning", "no file choosed")
        else:
            self.index = 0
            del self.database
            self.database = []
            self.database.append({"basename":os.path.splitext(os.path.basename(fname))[0], "fname":fname, "lname":None})
            self.update()
            self.logger.log_file("loaded file " + fname)


    def load_files(self):
        """ open wsi file directory dialog """
        file_dir = filedialog.askdirectory()
        if not file_dir:
            messagebox.showinfo("warning", "no directory choosed")
        else:
            self.index = None
            del self.database
            self.database = []
            fnames = os.listdir(file_dir)
            fnames.sort()
            for fname in fnames:
                if fname.endswith(".kfb") or fname.endswith(".tif"):
                    self.database.append({"basename":os.path.splitext(fname)[0], "fname":os.path.join(file_dir, fname), "lname":None})
            if not self.database:
                messagebox.showinfo("warning", "no kfb file exists")
            else:
                self.index = 0
                self.update()
                self.logger.log_file("loaded files from " + file_dir)


    def load_labels(self):
        """ open label file dialog """
        if self.index is None:
            messagebox.showinfo("error", "no kfb/tif file loaded")
            return
        lname = filedialog.askopenfilename(filetypes=(("csv files", "*.csv"), ("xml files", "*.xml")))
        if not lname:
            messagebox.showinfo("warning", "no file choosed")
        elif not self.database[self.index]["basename"] in os.path.basename(lname):
            messagebox.showinfo("warning", "label file does not match with kfb/tif file")
        else:
            self.database[self.index]["lname"] = lname
            self.update()
            self.logger.set_log_path(os.path.dirname(lname))
            self.logger.log_info("loaded label file " + lname)


    def load_labels_dir(self):
        """ open label file directory dialog """
        def nullify_lname():
            """ set lable name to None before loading new """
            for item in self.database:
                item["lname"] = None
        def choose_matched():
            """ choose only those wsi file name matches label file """
            database_new = [item for item in self.database if item["lname"] is not None]
            del self.database
            self.database = database_new

        if self.index is None:
            messagebox.showinfo("error", "no kfb/tif file loaded")
            return
        file_dir = filedialog.askdirectory()
        if not file_dir:
            messagebox.showinfo("warning", "no directory choosed")
        else:
            nullify_lname()
            lnames = os.listdir(file_dir)
            lnames.sort()
            for lname in lnames:
                if lname.endswith(".csv") or lname.endswith(".xml"):
                    for i,item in enumerate(self.database):
                        if item["basename"] in os.path.basename(lname):
                            self.database[i]["lname"] = os.path.join(file_dir, lname)
                            break
            choose_matched()
            self.index = 0 if self.database else None
            self.update()
            self.logger.set_log_path(file_dir)
            self.logger.log_info("loaded label files from " + file_dir)


    def update_patcher(self):
        """ initialize and update patcher """
        def need_reget_label():
            """ check if need to get label from patcher """
            # check if haven't got labels from patcher
            if not "labels" in self.database[self.index]:
                return True
            # check if have got labels from patcher, but with None label file
            # note: there is a case that this will lead to excess label getting,
            # that is, patcher.labels contains no labels.
            count = 0
            for class_i,boxes in self.database[self.index]["labels"].items():
                count += len(boxes)
            return count == 0

        # in case when patcher initialized without label file, need to reinitialize
        if need_reget_label():
            self.patcher = Patcher(self.database[self.index]["fname"], self.database[self.index]["lname"])
            self.database[self.index]["labels"] = self.patcher.get_labels()


    def load_thumbnail(self, blur):
        """ load thumbnail image, given selected classes """
        def resize(image, w, h):
            w0, h0 = image.size
            scale = min(w/w0, h/w0)
            return image.resize((int(w0*scale), int(h0*scale)))
        
        checked_classes = [cfg.CLASSES[i] for i,var in enumerate(self.checkboxes) if var.get()]
        image = self.patcher.patch_label({key:value for key,value in self.database[self.index]["labels"].items() if key in checked_classes}, blur)
        image = ImageTk.PhotoImage(resize(image, self.w*self.i, self.h))
        self.thumb_on = image  # stores the thumbnail image on first tab
        self.display.create_image(self.w*self.i/2, self.h/2, image=image)


    def update_text(self):
        """ tab1: update display text """
        self.dir_name.config(text=os.path.basename(os.path.dirname(self.database[self.index]["fname"])))
        self.n_count.config(text="{} / {}".format(self.index+1, len(self.database)))
        self.fname.config(text=os.path.basename(self.database[self.index]["fname"]))
        if self.database[self.index]["lname"] is not None:
            self.lname.config(text=os.path.basename(self.database[self.index]["lname"]))
        else:
            self.lname.config(text="--------")


    def update_label_counts(self):
        """ tab1: update label counts """
        for i,class_i in enumerate(cfg.CLASSES):
            self.colorblock[i].config(text=str(len(self.database[self.index]["labels"][class_i])))


    def clear(self):
        """ clear and update window when failed to load matching label files """
        self.dir_name.config(text="----")
        self.n_count.config(text="----")
        self.fname.config(text="----.kfb/.tif")
        self.lname.config(text="----.csv/.xml")
        self.thumb_on = None
        self.database = []
        for clb in self.colorblock:
            clb.config(text="--")


    def update(self, step=0, blur=False):
        """ update method of first tab """
        if self.index is None:
            messagebox.showinfo("error", "there is no file/label matched")
            self.clear()
            return
        if self.index+step not in range(len(self.database)):
            messagebox.showinfo("warning", "already the end")
            return
        self.index += step
        self.update_patcher()
        self.load_thumbnail(blur)
        self.update_text()
        self.update_label_counts()


    # below are functions relative to second tab
    def set_save_dir(self):
        """ set new label file saving directory, use source label file dir if not set """
        if self.index is None:
            messagebox.showinfo("error", "there is no file/label matched")
            return
        self.save_dir = filedialog.askdirectory()
        if not self.save_dir:
            messagebox.showinfo("warning", "no directory choosed, will use default")
        else:
            self.logger.log_info("set label file saving directory " + self.save_dir)


    def load_images(self):
        # update color
        self.update_color_i(finished=False)
        # get checked classes
        checked_classes = [cfg.CLASSES[i] for i,var in enumerate(self.checkboxes_i) if var.get()]
        # get images from patcher
        sub_labels = {key:value for key,value in self.database[self.index]["labels"].items() if key in checked_classes}
        self.image_list = self.patcher.crop_images(sub_labels, N=float(self.N.get()))
        self.cursor = 0  # stores the index of first image on canvas of second tab
        self.images_on = None  # stores the labeled images on second tab
        self.logger.log_info("selected classes: {}".format(checked_classes))

        # calculate anchor points for images
        M = int(self.M.get())  # number of images in a row
        self.size_avg = self.w * self.i / M  # average image size to display
        rows = int(self.h / self.size_avg)  # number of rows to display
        pad = (self.h-self.size_avg*rows)/rows  # padding between rows
        self.anchors = []  # stores the center position of images to show
        for row in range(rows):
            for i in range(M):
                self.anchors.append((int(self.size_avg/2 + self.size_avg*i), int(self.size_avg/2 + self.size_avg*row + pad*row)))


    def get_cursor_of_image(self, position):
        """ get current index of image in image_list, given position (event.x, event.y) """
        distance = {}
        cursor = self.cursor
        for anchor in self.anchors:
            if cursor not in range(len(self.image_list)):
                break
            distance[(position[0]-anchor[0])**2 + (position[1]-anchor[1])**2] = cursor
            cursor += 1
        sort_dist = sorted(distance.items())
        return sort_dist[0][1] if sort_dist else -1  # bug in canvas: still able to click when there are no images


    def get_label_by_cursor(self, cursor_of_image):
        """ 1. get key box from image_list, by index
            2. get label from labels, by key box
        """
        box = self.image_list[cursor_of_image][1]
        # search self.database[self.index]["labels"] for class_i
        for class_i,boxes in self.database[self.index]["labels"].items():
            if box in boxes:
                return class_i
        return "DELETED!!!"


    def on_single_click(self, event):
        """ on single click, display label of current image """
        def show_label():
            # destroy any toplevel window
            destroy()
            # creates a toplevel window
            self.tw = tk.Toplevel(self.display_i)
            # Leaves only the frame and removes the app window
            self.tw.wm_overrideredirect(True)
            win = tk.Frame(self.tw, borderwidth=0)
            lbl = ttk.Label(win, text=label, justify=tk.LEFT,
                            relief=tk.SOLID, borderwidth=0)
            pad = (5, 3, 5, 3)
            lbl.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW)
            win.grid()
            # set the position of label to show on screen
            x_of_c, y_of_c = self.display_i.winfo_rootx(), self.display_i.winfo_rooty()
            x, y = x_can + x_of_c + 5, y_can + y_of_c - 25  # x_can,y_can are defined outside
            self.tw.wm_geometry("+%d+%d" % (x, y))

        def destroy():
            if hasattr(self, "tw") and self.tw:
                self.tw.destroy()
            self.tw = None

        def wait_and_hide():
            waittime = 500  # in miniseconds
            self.display_i.after(waittime, destroy)

        # get mouse position, relative to canvas
        x_can, y_can = event.x, event.y
        # get the index of image in self.image_list
        cursor_of_image = self.get_cursor_of_image((x_can, y_can))
        if cursor_of_image == -1:
            return
        # get the lable of the image
        label = self.get_label_by_cursor(cursor_of_image)
        # show label on screen
        show_label()
        # hide label after some time
        wait_and_hide()


    def on_double_click(self, event):
        """ on double click, popup a new image window, being able change image cropping size """
        def show_image():
            # destroy existing toplevel window
            destroy()
            # create a toplevel window and configure window size
            self.tw_i = tk.Toplevel(self.display_i)
            w, h = self.image_list[cursor_of_image][2].size
            scale = min(1.0, min(self.w/w, self.h/h))
            w, h = int(w*scale), int(h*scale)
            self.tw_i.wm_geometry("{}x{}+{}+{}".format(w, h, int(self.w/2-w/2), int(self.h/2-h/2)))
            self.tw_i_x = float(self.N.get())  # stores the times of image dimension over cell dimension
            self.tw_i.title(label+" {}x".format(self.tw_i_x))
            # add decrease and increase menu 
            menubar = tk.Menu(self.tw_i)
            sizemenu = tk.Menu(menubar, tearoff=0)
            sizemenu.add_command(label="decrease", command=lambda: resize(delta=-1))
            sizemenu.add_separator()
            sizemenu.add_command(label="increase", command=lambda: resize(delta=1))
            menubar.add_cascade(label="change size", menu=sizemenu)
            self.tw_i.config(menu=menubar)
            # add image to canvas
            self.tw_i_can = tk.Canvas(self.tw_i)
            self.tw_i_can.pack(fill=tk.BOTH, expand=tk.YES)
            self.image_tw = ImageTk.PhotoImage(self.image_list[cursor_of_image][2].resize((w, h)))
            self.tw_i_can.create_image(w//2, h//2, image=self.image_tw)

        def resize(delta):
            self.tw_i_x += delta
            if self.tw_i_x < 1.0:
                # already down to the cell, cannot cut smaller
                self.tw_i_x -= delta
                return
            image = self.patcher.get_cell_by_N(box=self.image_list[cursor_of_image][1], N=self.tw_i_x)
            w, h = image.size
            scale = min(1.0, min(self.w/w, self.h/h))
            w, h = int(w*scale), int(h*scale)
            self.tw_i.title(label+" {}x".format(self.tw_i_x))
            self.tw_i.wm_geometry("{}x{}".format(w, h))
            self.image_tw = ImageTk.PhotoImage(image.resize((w, h)))
            self.tw_i_can.delete("all")
            self.tw_i_can.config(width=w, height=h)
            self.tw_i_can.create_image(w//2, h//2, image=self.image_tw)

        def destroy():
            if hasattr(self, "tw_i") and self.tw_i:
                self.tw_i.destroy()
            self.tw_i = None

        # get mouse position, relative to canvas
        x_can, y_can = event.x, event.y
        # get the index of image in self.image_list
        cursor_of_image = self.get_cursor_of_image((x_can, y_can))
        if cursor_of_image == -1:
            return
        # get the lable of the image
        label = self.get_label_by_cursor(cursor_of_image)
        # display image
        show_image()


    def on_right_click(self, event):
        """ on double click, popup radiobox selection panel, for label change or delete """
        def show_choices():
            # destroy existing toplevel window
            destroy()
            # creates a toplevel window
            self.tw = tk.Toplevel(self.display_i)
            # Leaves only the frame and removes the app window
            self.tw.wm_overrideredirect(True)
            win = tk.Frame(self.tw, borderwidth=0)
            self.radioBox = StringVar(value=label)
            # delete choice
            tk.Radiobutton(win, text="DELETE", padx=5, variable=self.radioBox, 
                                command=choice_made, 
                                value="DELETE", fg="#ff0000", activeforeground="#ff0000").pack(anchor=tk.W)
            # choices to be change label to
            for class_i in cfg.CLASSES:
                tk.Radiobutton(win, text=class_i, padx=5, variable=self.radioBox,
                                    command=choice_made, 
                                    value=class_i).pack(anchor=tk.W)
            win.grid()
            # set the position of label to show on screen
            # first get the position of canvas (upleft) on screen
            x_of_c, y_of_c = self.display_i.winfo_rootx(), self.display_i.winfo_rooty()
            # then add the position of mouse on canvas 
            x, y = x_can + x_of_c + 5, y_can + y_of_c - 150
            # adjust x,y if tw falls out of window
            x = min(x, self.w-100)
            y = min(y, self.h-400)
            y = max(y, 10)
            self.tw.wm_geometry("+%d+%d" % (x, y))  

        def destroy():
            if hasattr(self, "tw") and self.tw:
                self.tw.destroy()
            self.tw = None

        def choice_made():
            waittime = 300  # in miniseconds
            self.display_i.after(waittime, destroy)

            # retrieve choice and make changes accordingly
            choice = self.radioBox.get()
            # detect if there are changes made
            if choice == label or (label == "DELETED!!!" and choice == "DELETE"):
                return
            box = self.image_list[cursor_of_image][1]           
            # change original label to choice
            if choice != "DELETE":
                self.database[self.index]["labels"][choice][box] = self.database[self.index]["labels"][label][box]
                outline_color = "blue"
                self.image_list[cursor_of_image][0] = choice  # update class_i in image_list
                self.logger.log_change("{} {}".format(box,label), "{}".format(choice))
            else:  # when choice == "DELETE"
                if "DELETED!!!" not in self.database[self.index]["labels"]:  # add new key to labels
                    self.database[self.index]["labels"]["DELETED!!!"] = {}
                self.database[self.index]["labels"]["DELETED!!!"][box] = self.database[self.index]["labels"][label][box]
                outline_color = "red"
                self.image_list[cursor_of_image][0] = "DELETED!!!"  # update class_i in image_list, note: new key here
                self.logger.log_delete("{} {}".format(box,label))
            # delete label
            del self.database[self.index]["labels"][label][box]
            # update label counts
            self.update_label_counts_i()
            # add surrounding rectangle to changed image on canvas
            self.image_list[cursor_of_image].append(outline_color)
            anchor = self.anchors[cursor_of_image - self.cursor]
            x0, y0 = anchor[0] - self.size_avg/2 + 1, anchor[1] - self.size_avg/2 + 1
            x1, y1 = anchor[0] + self.size_avg/2 - 1, anchor[1] + self.size_avg/2 - 1
            self.display_i.create_rectangle(x0, y0, x1, y1, outline=outline_color, width=2)
                
        # get mouse position, relative to canvas
        x_can, y_can = event.x, event.y
        # get the index of image in self.image_list
        cursor_of_image = self.get_cursor_of_image((x_can, y_can))
        if cursor_of_image == -1:
            return
        # get the lable of the image
        label = self.get_label_by_cursor(cursor_of_image)
        # # check if label has been deleted
        # if label == "DELETED!!!":
        #     messagebox.showinfo("warning", "image has been deleted")
        # show label choose dialog on screen
        show_choices()
    

    """ key press doesn't work after canvas loading images
    def on_d_pressed(self, event):
        print("d pressed at {}, ({},{})".format(self.display_i.type(tk.CURRENT), event.x, event.y))
        if self.display_i.type(tk.CURRENT) != "image":
            return
        # get mouse position, relative to canvas
        x_can, y_can = event.x, event.y
        # get the index of image in self.image_list
        cursor_of_image = self.get_cursor_of_image((x_can, y_can))
        # get the label of the image
        label = self.get_label_by_cursor(cursor_of_image)
        if label == "DELETED!!!":
            return
        # delete label
        box = self.image_list[cursor_of_image][1] 
        if "DELETED!!!" not in self.database[self.index]["labels"]:
            self.database[self.index]["labels"]["DELETED!!!"] = {}
        self.database[self.index]["labels"]["DELETED!!!"][box] = self.database[self.index]["labels"][label][box]
        outline_color = "red"
        self.image_list[cursor_of_image][0] = "DELETED!!!"  # update class_i in image_list, note: new key here
        self.logger.log_delete("{} {}".format(box,label))
        del self.database[self.index]["labels"][label][box]
        # update label counts
        self.update_label_counts_i()
        # add surrounding rectangle to changed image on canvas
        self.image_list[cursor_of_image].append(outline_color)
        anchor = self.anchors[cursor_of_image - self.cursor]
        x0, y0 = anchor[0] - self.size_avg/2 + 1, anchor[1] - self.size_avg/2 + 1
        x1, y1 = anchor[0] + self.size_avg/2 - 1, anchor[1] + self.size_avg/2 - 1
        self.display_i.create_rectangle(x0, y0, x1, y1, outline=outline_color, width=2)
    """


    def update_images(self, step):
        """ update display images on canvas """
        def resize(image, size):
            w, h = image.size
            scale = min(1, min(size/w, size/h))
            return image.resize((int(w*scale), int(h*scale)))

        # update color, when last batch of images are displayed
        if self.cursor + 2*len(self.anchors) >= len(self.image_list):
            self.update_color_i(finished=True)

        # update cursor, stay unchanged if running to the end
        self.cursor += step * len(self.anchors)
        if self.cursor not in range(len(self.image_list)):
            self.cursor -= step * len(self.anchors)
            messagebox.showinfo("warning", "no more images")
            return

        # update images
        del self.images_on
        self.images_on = []
        self.display_i.delete("all")  # delete all objects before loading
        cursor = self.cursor
        for anchor in self.anchors:
            if cursor not in range(len(self.image_list)):
                break
            image = ImageTk.PhotoImage(resize(self.image_list[cursor][2], self.size_avg))
            self.images_on.append(image)
            self.display_i.create_image(anchor[0], anchor[1], image=image, tags="image_{}".format(cursor))
            self.display_i.tag_bind("image_{}".format(cursor), "<ButtonPress-1>", self.on_single_click)
            self.display_i.tag_bind("image_{}".format(cursor), "<Double-Button-1>", self.on_double_click)
            self.display_i.tag_bind("image_{}".format(cursor), "<Button-3>", self.on_right_click)
            # add bounding rectangle for changed images, on canvas
            if len(self.image_list[cursor]) > 3:
                outline_color = self.image_list[cursor][-1]  # get the last color, should i only store one instead?
                x0, y0 = anchor[0] - self.size_avg/2 + 1, anchor[1] - self.size_avg/2 + 1
                x1, y1 = anchor[0] + self.size_avg/2 - 1, anchor[1] + self.size_avg/2 - 1
                self.display_i.create_rectangle(x0, y0, x1, y1, outline=outline_color, width=2)                
            cursor += 1


    def load_thumbnailmini(self):
        """ load mini thumbnail image, in bottom left corner """
        def resize(image, w, h):
            w0, h0 = image.size
            scale = min(w/w0, h/w0)
            return image.resize((int(w0*scale), int(h0*scale)))

        sub_labels = {}
        for i in range(len(self.anchors)):
            cursor = self.cursor + i
            if cursor not in range(len(self.image_list)):
                break
            class_i = self.image_list[cursor][0]
            if class_i == "DELETED!!!":  # omit deleted labels
                continue
            box = self.image_list[cursor][1]
            if class_i not in sub_labels:
                sub_labels[class_i] = {}
            sub_labels[class_i][box] = self.database[self.index]["labels"][class_i][box]
        
        image = self.patcher.patch_label(sub_labels, blur=True)
        image = ImageTk.PhotoImage(resize(image, self.w_left_i, self.w_left_i))
        self.thumbmini_on = image  # stores the mini thumbnail image on second tab
        self.thumbmini.create_image(self.w_left_i/2, self.w_left_i/2, image=image)


    def update_label_counts_i(self, clear=False):
        """ update label counts in second tab """
        if clear:
            for clb in self.colorblock_i:
                clb.config(text="--")
        else:
            for i,clb in enumerate(self.colorblock_i):
                clb.config(text=str(len(self.database[self.index]["labels"][cfg.CLASSES[i]])))
            # for i,class_i in enumerate(cfg.CLASSES):
            #     self.colorblock_i[i].config(text=str(len(self.database[self.index]["labels"][class_i])))

    def update_text_i(self, clear=False):
        """ update 1. file view progress and 2. image view progress """
        if clear:
            self.n_count_i.config(text="no progress")
        else:
            self.n_count_i.config(text="files: {} / {}".format(self.index+1, len(self.database)))
        if hasattr(self, "cursor"):
            self.image_pro.config(text="images in view: {} / {}".format(self.cursor+1, len(self.image_list)))
        else:
            self.image_pro.config(text="no progress")

    def update_color_i(self, finished=False, clear=False):
        """ change colorbox color
        :param finished: the status of viewing images. Change color to shallow yellow if started viewing, green if finished.
        :param clear: the command to reset color to white, at tab changes
        """
        if clear:
            for clb in self.colorblock_i:
                clb.configure(background="#ffffff")
            return
        color = "#00ff00" if finished else "#ffff99"
        checked_classes = [i for i,var in enumerate(self.checkboxes_i) if var.get()]
        for i,clb in enumerate(self.colorblock_i):
            if i in checked_classes:
                clb.configure(background=color)


    def update_i(self, step):
        """ update method for second tab """
        if self.index is None:
            messagebox.showinfo("error", "there is no file/label matched")
            return
        if step == 0:
            self.load_images()
        if hasattr(self, "cursor"):
            self.update_images(step=step)
            self.load_thumbnailmini()
        self.update_text_i()
        self.update_label_counts_i()


    def save_labels(self, index=None):
        """ upload labels changes to patcher """
        if hasattr(self, "patcher"):
            if index is None:
                if self.index is None:
                    return
                else:
                    index = self.index
            if index not in range(len(self.database)):  # happens when failed to load new label files
                return
            self.patcher.set_labels(self.database[index]["labels"])
            if self.database[index]["lname"] is None:  # happens when no label file is loaded
                return
            # use the same directory if new label file dir is not set
            if not hasattr(self, "save_dir") or not self.save_dir:
                self.save_dir = os.path.dirname(self.database[index]["lname"])
            label_file = os.path.join(self.save_dir, self.database[index]["basename"]+".xml")
            self.patcher.write_labels(label_file)
            self.logger.log_save(self.database[index]["basename"])


    def on_visibility(self, event):
        """ clear up and update second tab upon tab switch, when self.index changed in first tab """
        if not hasattr(self, "old_index"):
            self.old_index = None
            self.save_dir = None
        if self.old_index != self.index:
            self.thumbmini.delete("all")  # delete thumbnail image
            self.display_i.delete("all")  # delete all images on canvas
            self.update_color_i(clear=True)  # reset background color in colorboxes
            if self.old_index is not None:  # save labels to label file
                self.save_labels(self.old_index)
            if self.index is None:  # happens when failed to load new label files
                self.update_text_i(clear=True)
                self.update_label_counts_i(clear=True)  # clear label counts on second tab
            else:
                self.update_text_i()
                self.update_label_counts_i()  # update label counts on second tab
                self.logger.log_open(self.database[self.index]["basename"])
            self.old_index = self.index


    def on_close(self):
        """ actions performed when window closed """
        try:
            self.save_labels(self.index)
            self.logger.on_close()
            del self.database
            self.root.destroy()
        except:
            print("failed to clear up program.")


    def run(self):
        self.root.mainloop()