diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c5c32eb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/Images +!/Images/001 +/Labels +!Labels/001 \ No newline at end of file diff --git a/Examples/001/Cute-Dog-Wallpaper.JPEG b/Examples/demo/Cute-Dog-Wallpaper.JPG similarity index 100% rename from Examples/001/Cute-Dog-Wallpaper.JPEG rename to Examples/demo/Cute-Dog-Wallpaper.JPG diff --git a/Examples/001/dog.JPEG b/Examples/demo/dog.JPG similarity index 100% rename from Examples/001/dog.JPEG rename to Examples/demo/dog.JPG diff --git a/Examples/001/dogs-wallpaper.JPEG b/Examples/demo/dogs-wallpaper.JPG similarity index 100% rename from Examples/001/dogs-wallpaper.JPEG rename to Examples/demo/dogs-wallpaper.JPG diff --git a/Images/001/test.JPEG b/Images/001/test.JPG similarity index 100% rename from Images/001/test.JPEG rename to Images/001/test.JPG diff --git a/Images/001/test2.JPEG b/Images/001/test2.JPG similarity index 100% rename from Images/001/test2.JPEG rename to Images/001/test2.JPG diff --git a/Images/001/test3.JPEG b/Images/001/test3.JPG similarity index 100% rename from Images/001/test3.JPEG rename to Images/001/test3.JPG diff --git a/Labels/001/test.txt b/Labels/001/test.txt index 808509e1..9aded27f 100644 --- a/Labels/001/test.txt +++ b/Labels/001/test.txt @@ -1,2 +1,3 @@ -1 -80 8 172 111 +2 +80 12 174 110 dog +17 49 72 110 cat diff --git a/Labels/001/test2.txt b/Labels/001/test2.txt deleted file mode 100644 index 22e8a683..00000000 --- a/Labels/001/test2.txt +++ /dev/null @@ -1,2 +0,0 @@ -1 -129 42 389 235 diff --git a/Labels/001/test3.txt b/Labels/001/test3.txt deleted file mode 100644 index e51f2880..00000000 --- a/Labels/001/test3.txt +++ /dev/null @@ -1,2 +0,0 @@ -1 -74 73 171 170 diff --git a/README.md b/README.md index 5c823f37..f333ff61 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ +### Forked from [puzzledqs/BBox-Label-Tool](https://github.com/puzzledqs/BBox-Label-Tool) +## Improvements +1. Add multi-class support +2. Change some of the color-candidates for better display +3. Fix the 'Example' filepath for convenience +4. Change the image format from '.JPEG' to '.JPG' + +## New Usage +### For multi-class task, modify 'class.txt' with your own class-candidates and before labeling bbox, choose the 'Current Class' in the Combobox and make sure you click 'ComfirmClass' button. + +### The remaining usage is the same as the origin one. + +------------------------------------ + +**Contact info**: jxgu1016@gmail.com + +------------------------------------ + BBox-Label-Tool =============== diff --git a/class.txt b/class.txt new file mode 100644 index 00000000..0e678053 --- /dev/null +++ b/class.txt @@ -0,0 +1,5 @@ + +person +dog +cat +baby diff --git a/main.py b/main.py index e19a0888..40e4a391 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,36 @@ -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Name: Object bounding box label tool # Purpose: Label object bboxes for ImageNet Detection data # Author: Qiushi # Created: 06/06/2014 # -#------------------------------------------------------------------------------- -from __future__ import division -from Tkinter import * -import tkMessageBox +# ------------------------------------------------------------------------------ + +from __future__ import division, print_function + +import itertools + +try: + from Tkinter import * + import tkMessageBox + import ttk +except ImportError: + from tkinter import * + from tkinter import messagebox as tkMessageBox + from tkinter import ttk + from PIL import Image, ImageTk import os import glob import random # colors for the bboxes -COLORS = ['red', 'blue', 'yellow', 'pink', 'cyan', 'green', 'black'] +COLORS = ['red', 'blue', 'olive', 'teal', 'cyan', 'green', 'black'] # image sizes for the examples SIZE = 256, 256 + class LabelTool(): def __init__(self, master): # set up the main frame @@ -26,11 +38,11 @@ def __init__(self, master): self.parent.title("LabelTool") self.frame = Frame(self.parent) self.frame.pack(fill=BOTH, expand=1) - self.parent.resizable(width = FALSE, height = FALSE) + self.parent.resizable(width=FALSE, height=FALSE) # initialize global state - self.imageDir = '' - self.imageList= [] + self.image_dir = 'Images' + self.image_list = [] self.egDir = '' self.egList = [] self.outDir = '' @@ -40,11 +52,12 @@ def __init__(self, master): self.imagename = '' self.labelfilename = '' self.tkimg = None + self.current_label_class = '' + self.cla_can_temp = [] + self.classcandidate_filename = 'class.txt' # initialize mouse state - self.STATE = {} - self.STATE['click'] = 0 - self.STATE['x'], self.STATE['y'] = 0, 0 + self.STATE = {'click': 0, 'x': 0, 'y': 0} # reference to bbox self.bboxIdList = [] @@ -55,105 +68,114 @@ def __init__(self, master): # ----------------- GUI stuff --------------------- # dir entry & load - self.label = Label(self.frame, text = "Image Dir:") - self.label.grid(row = 0, column = 0, sticky = E) + self.label = Label(self.frame, text="Image Dir:") + self.label.grid(row=0, column=0, sticky=E) self.entry = Entry(self.frame) - self.entry.grid(row = 0, column = 1, sticky = W+E) - self.ldBtn = Button(self.frame, text = "Load", command = self.loadDir) - self.ldBtn.grid(row = 0, column = 2, sticky = W+E) + self.entry.insert(0, self.image_dir) + self.entry.grid(row=0, column=1, sticky=W + E) + self.ldBtn = Button(self.frame, text="Load", command=self.load_dir) + self.ldBtn.grid(row=0, column=2, sticky=W + E) # main panel for labeling self.mainPanel = Canvas(self.frame, cursor='tcross') self.mainPanel.bind("", self.mouseClick) self.mainPanel.bind("", self.mouseMove) - self.parent.bind("", self.cancelBBox) # press to cancel current bbox - self.parent.bind("s", self.cancelBBox) - self.parent.bind("a", self.prevImage) # press 'a' to go backforward - self.parent.bind("d", self.nextImage) # press 'd' to go forward - self.mainPanel.grid(row = 1, column = 1, rowspan = 4, sticky = W+N) + self.parent.bind("", self.cancelBBox) # press to cancel current bbox + self.mainPanel.bind("s", self.cancelBBox) + self.mainPanel.bind("a", self.prevImage) # press 'a' to go backward + self.mainPanel.bind("d", self.nextImage) # press 'd' to go forward + self.mainPanel.grid(row=1, column=1, rowspan=4, sticky=W + N) + + # choose class + self.classname = StringVar() + self.classcandidate = ttk.Combobox(self.frame, state='readonly', textvariable=self.classname) + self.classcandidate.grid(row=1, column=2) + if os.path.exists(self.classcandidate_filename): + with open(self.classcandidate_filename) as cf: + for line in cf.readlines(): + # print line + self.cla_can_temp.append(line.strip('\n')) + # print self.cla_can_temp + self.classcandidate['values'] = self.cla_can_temp + self.classcandidate.current(0) + self.current_label_class = self.classcandidate.get() # init + self.btnclass = Button(self.frame, text='Confirm Class', command=self.set_class) + self.btnclass.grid(row=2, column=2, sticky=W + E) # showing bbox info & delete bbox - self.lb1 = Label(self.frame, text = 'Bounding boxes:') - self.lb1.grid(row = 1, column = 2, sticky = W+N) - self.listbox = Listbox(self.frame, width = 22, height = 12) - self.listbox.grid(row = 2, column = 2, sticky = N) - self.btnDel = Button(self.frame, text = 'Delete', command = self.delBBox) - self.btnDel.grid(row = 3, column = 2, sticky = W+E+N) - self.btnClear = Button(self.frame, text = 'ClearAll', command = self.clearBBox) - self.btnClear.grid(row = 4, column = 2, sticky = W+E+N) + self.lb1 = Label(self.frame, text='Bounding boxes:') + self.lb1.grid(row=3, column=2, sticky=W + N) + self.listbox = Listbox(self.frame, width=22, height=12) + self.listbox.grid(row=4, column=2, sticky=N + S) + self.btnDel = Button(self.frame, text='Delete', command=self.delBBox) + self.btnDel.grid(row=5, column=2, sticky=W + E + N) + self.btnClear = Button(self.frame, text='Clear All', command=self.clearBBox) + self.btnClear.grid(row=6, column=2, sticky=W + E + N) # control panel for image navigation self.ctrPanel = Frame(self.frame) - self.ctrPanel.grid(row = 5, column = 1, columnspan = 2, sticky = W+E) - self.prevBtn = Button(self.ctrPanel, text='<< Prev', width = 10, command = self.prevImage) - self.prevBtn.pack(side = LEFT, padx = 5, pady = 3) - self.nextBtn = Button(self.ctrPanel, text='Next >>', width = 10, command = self.nextImage) - self.nextBtn.pack(side = LEFT, padx = 5, pady = 3) - self.progLabel = Label(self.ctrPanel, text = "Progress: / ") - self.progLabel.pack(side = LEFT, padx = 5) - self.tmpLabel = Label(self.ctrPanel, text = "Go to Image No.") - self.tmpLabel.pack(side = LEFT, padx = 5) - self.idxEntry = Entry(self.ctrPanel, width = 5) - self.idxEntry.pack(side = LEFT) - self.goBtn = Button(self.ctrPanel, text = 'Go', command = self.gotoImage) - self.goBtn.pack(side = LEFT) + self.ctrPanel.grid(row=7, column=1, columnspan=2, sticky=W + E) + self.prevBtn = Button(self.ctrPanel, text='<< Prev', width=10, command=self.prevImage) + self.prevBtn.pack(side=LEFT, padx=5, pady=3) + self.nextBtn = Button(self.ctrPanel, text='Next >>', width=10, command=self.nextImage) + self.nextBtn.pack(side=LEFT, padx=5, pady=3) + self.progLabel = Label(self.ctrPanel, text="Progress: / ") + self.progLabel.pack(side=LEFT, padx=5) + self.tmpLabel = Label(self.ctrPanel, text="Go to Image No.") + self.tmpLabel.pack(side=LEFT, padx=5) + self.idxEntry = Entry(self.ctrPanel, width=5) + self.idxEntry.pack(side=LEFT) + self.goBtn = Button(self.ctrPanel, text='Go', command=self.gotoImage) + self.goBtn.pack(side=LEFT) # example pannel for illustration - self.egPanel = Frame(self.frame, border = 10) - self.egPanel.grid(row = 1, column = 0, rowspan = 5, sticky = N) - self.tmpLabel2 = Label(self.egPanel, text = "Examples:") - self.tmpLabel2.pack(side = TOP, pady = 5) + self.egPanel = Frame(self.frame, border=10) + self.egPanel.grid(row=1, column=0, rowspan=5, sticky=N) + self.tmpLabel2 = Label(self.egPanel, text="Examples:") + self.tmpLabel2.pack(side=TOP, pady=5) self.egLabels = [] for i in range(3): self.egLabels.append(Label(self.egPanel)) - self.egLabels[-1].pack(side = TOP) + self.egLabels[-1].pack(side=TOP) # display mouse position self.disp = Label(self.ctrPanel, text='') - self.disp.pack(side = RIGHT) + self.disp.pack(side=RIGHT) - self.frame.columnconfigure(1, weight = 1) - self.frame.rowconfigure(4, weight = 1) + self.frame.columnconfigure(1, weight=1) + self.frame.rowconfigure(4, weight=1) - # for debugging -## self.setImage() -## self.loadDir() + def load_dir(self): + value = self.entry.get() + if not os.path.isdir(value): + print('{} is not a directory'.format(value)) + return - def loadDir(self, dbg = False): - if not dbg: - s = self.entry.get() - self.parent.focus() - self.category = int(s) - else: - s = r'D:\workspace\python\labelGUI' -## if not os.path.isdir(s): -## tkMessageBox.showerror("Error!", message = "The specified dir doesn't exist!") -## return - # get image list - self.imageDir = os.path.join(r'./Images', '%03d' %(self.category)) - self.imageList = glob.glob(os.path.join(self.imageDir, '*.JPEG')) - if len(self.imageList) == 0: - print 'No .JPEG images found in the specified dir!' + self.image_dir = value + + self.image_list = glob_images(self.image_dir) + if len(self.image_list) == 0: + print('No images found in the specified dir <%s>!' % self.image_dir) return # default to the 1st image in the collection self.cur = 1 - self.total = len(self.imageList) + self.total = len(self.image_list) - # set up output dir - self.outDir = os.path.join(r'./Labels', '%03d' %(self.category)) + # set up output dir + self.outDir = './Labels' if not os.path.exists(self.outDir): os.mkdir(self.outDir) # load example bboxes - self.egDir = os.path.join(r'./Examples', '%03d' %(self.category)) - if not os.path.exists(self.egDir): - return - filelist = glob.glob(os.path.join(self.egDir, '*.JPEG')) + self.egDir = os.path.join(os.path.dirname(__file__), 'Examples/demo') + assert os.path.exists(self.egDir) + + file_list = glob.glob(os.path.join(self.egDir, '*.JPG')) self.tmp = [] self.egList = [] - random.shuffle(filelist) - for (i, f) in enumerate(filelist): + random.shuffle(file_list) + for (i, f) in enumerate(file_list): if i == 3: break im = Image.open(f) @@ -161,50 +183,51 @@ def loadDir(self, dbg = False): new_size = int(r * im.size[0]), int(r * im.size[1]) self.tmp.append(im.resize(new_size, Image.ANTIALIAS)) self.egList.append(ImageTk.PhotoImage(self.tmp[-1])) - self.egLabels[i].config(image = self.egList[-1], width = SIZE[0], height = SIZE[1]) + self.egLabels[i].config(image=self.egList[-1], width=SIZE[0], height=SIZE[1]) self.loadImage() - print '%d images loaded from %s' %(self.total, s) + print('%d images loaded from %s' % (self.total, self.image_dir)) def loadImage(self): # load image - imagepath = self.imageList[self.cur - 1] + imagepath = self.image_list[self.cur - 1] self.img = Image.open(imagepath) self.tkimg = ImageTk.PhotoImage(self.img) - self.mainPanel.config(width = max(self.tkimg.width(), 400), height = max(self.tkimg.height(), 400)) - self.mainPanel.create_image(0, 0, image = self.tkimg, anchor=NW) - self.progLabel.config(text = "%04d/%04d" %(self.cur, self.total)) + self.mainPanel.config(width=max(self.tkimg.width(), 400), height=max(self.tkimg.height(), 400)) + self.mainPanel.create_image(0, 0, image=self.tkimg, anchor=NW) + self.progLabel.config(text="%04d/%04d" % (self.cur, self.total)) # load labels self.clearBBox() - self.imagename = os.path.split(imagepath)[-1].split('.')[0] + self.imagename = extract_basename(imagepath) labelname = self.imagename + '.txt' self.labelfilename = os.path.join(self.outDir, labelname) - bbox_cnt = 0 if os.path.exists(self.labelfilename): with open(self.labelfilename) as f: for (i, line) in enumerate(f): if i == 0: - bbox_cnt = int(line.strip()) + # bbox_cnt = int(line.strip()) continue - tmp = [int(t.strip()) for t in line.split()] -## print tmp + + tmp = line.split() self.bboxList.append(tuple(tmp)) - tmpId = self.mainPanel.create_rectangle(tmp[0], tmp[1], \ - tmp[2], tmp[3], \ - width = 2, \ - outline = COLORS[(len(self.bboxList)-1) % len(COLORS)]) + tmpId = self.mainPanel.create_rectangle(int(tmp[0]), int(tmp[1]), + int(tmp[2]), int(tmp[3]), + width=2, + outline=COLORS[(len(self.bboxList) - 1) % len(COLORS)]) + # print tmpId self.bboxIdList.append(tmpId) - self.listbox.insert(END, '(%d, %d) -> (%d, %d)' %(tmp[0], tmp[1], tmp[2], tmp[3])) - self.listbox.itemconfig(len(self.bboxIdList) - 1, fg = COLORS[(len(self.bboxIdList) - 1) % len(COLORS)]) + self.listbox.insert(END, '%s : (%d, %d) -> (%d, %d)' % (tmp[4], int(tmp[0]), int(tmp[1]), + int(tmp[2]), int(tmp[3]))) + self.listbox.itemconfig(len(self.bboxIdList) - 1, + fg=COLORS[(len(self.bboxIdList) - 1) % len(COLORS)]) def saveImage(self): with open(self.labelfilename, 'w') as f: - f.write('%d\n' %len(self.bboxList)) + f.write('%d\n' % len(self.bboxList)) for bbox in self.bboxList: f.write(' '.join(map(str, bbox)) + '\n') - print 'Image No. %d saved' %(self.cur) - + print('Labels for image no. {} ({}) saved'.format(self.cur, self.imagename)) def mouseClick(self, event): if self.STATE['click'] == 0: @@ -212,29 +235,29 @@ def mouseClick(self, event): else: x1, x2 = min(self.STATE['x'], event.x), max(self.STATE['x'], event.x) y1, y2 = min(self.STATE['y'], event.y), max(self.STATE['y'], event.y) - self.bboxList.append((x1, y1, x2, y2)) + self.bboxList.append((x1, y1, x2, y2, self.current_label_class)) self.bboxIdList.append(self.bboxId) self.bboxId = None - self.listbox.insert(END, '(%d, %d) -> (%d, %d)' %(x1, y1, x2, y2)) - self.listbox.itemconfig(len(self.bboxIdList) - 1, fg = COLORS[(len(self.bboxIdList) - 1) % len(COLORS)]) + self.listbox.insert(END, '%s : (%d, %d) -> (%d, %d)' % (self.current_label_class, x1, y1, x2, y2)) + self.listbox.itemconfig(len(self.bboxIdList) - 1, fg=COLORS[(len(self.bboxIdList) - 1) % len(COLORS)]) self.STATE['click'] = 1 - self.STATE['click'] def mouseMove(self, event): - self.disp.config(text = 'x: %d, y: %d' %(event.x, event.y)) + self.disp.config(text='x: %d, y: %d' % (event.x, event.y)) if self.tkimg: if self.hl: self.mainPanel.delete(self.hl) - self.hl = self.mainPanel.create_line(0, event.y, self.tkimg.width(), event.y, width = 2) + self.hl = self.mainPanel.create_line(0, event.y, self.tkimg.width(), event.y, width=2) if self.vl: self.mainPanel.delete(self.vl) - self.vl = self.mainPanel.create_line(event.x, 0, event.x, self.tkimg.height(), width = 2) + self.vl = self.mainPanel.create_line(event.x, 0, event.x, self.tkimg.height(), width=2) if 1 == self.STATE['click']: if self.bboxId: self.mainPanel.delete(self.bboxId) - self.bboxId = self.mainPanel.create_rectangle(self.STATE['x'], self.STATE['y'], \ - event.x, event.y, \ - width = 2, \ - outline = COLORS[len(self.bboxList) % len(COLORS)]) + self.bboxId = self.mainPanel.create_rectangle(self.STATE['x'], self.STATE['y'], + event.x, event.y, + width=2, + outline=COLORS[len(self.bboxList) % len(COLORS)]) def cancelBBox(self, event): if 1 == self.STATE['click']: @@ -245,7 +268,7 @@ def cancelBBox(self, event): def delBBox(self): sel = self.listbox.curselection() - if len(sel) != 1 : + if len(sel) != 1: return idx = int(sel[0]) self.mainPanel.delete(self.bboxIdList[idx]) @@ -260,13 +283,13 @@ def clearBBox(self): self.bboxIdList = [] self.bboxList = [] - def prevImage(self, event = None): + def prevImage(self, event=None): self.saveImage() if self.cur > 1: self.cur -= 1 self.loadImage() - def nextImage(self, event = None): + def nextImage(self, event=None): self.saveImage() if self.cur < self.total: self.cur += 1 @@ -274,20 +297,40 @@ def nextImage(self, event = None): def gotoImage(self): idx = int(self.idxEntry.get()) - if 1 <= idx and idx <= self.total: + if 1 <= idx <= self.total: self.saveImage() self.cur = idx self.loadImage() -## def setImage(self, imagepath = r'test2.png'): -## self.img = Image.open(imagepath) -## self.tkimg = ImageTk.PhotoImage(self.img) -## self.mainPanel.config(width = self.tkimg.width()) -## self.mainPanel.config(height = self.tkimg.height()) -## self.mainPanel.create_image(0, 0, image = self.tkimg, anchor=NW) + def set_class(self): + self.current_label_class = self.classcandidate.get() + print("set label class to : '{}'".format(self.current_label_class)) + + +# utils + +IMAGE_EXTENSIONS = [ + '*.jpg', + '*.jpeg', + '*.png', + '*.gif', + '*.bmp' +] + + +def glob_images(directory): + return list( + itertools.chain(*[glob.glob(os.path.join(directory, extension)) + for extension in IMAGE_EXTENSIONS]) + ) + + +def extract_basename(imagepath): + return os.path.split(imagepath)[-1].split('.')[0] + if __name__ == '__main__': root = Tk() tool = LabelTool(root) - root.resizable(width = True, height = True) + root.resizable(width=True, height=True) root.mainloop() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..7e2fba5e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Pillow