├── Examples └── 001 │ ├── Cute-Dog-Wallpaper.JPEG │ ├── dog.JPEG │ └── dogs-wallpaper.JPEG ├── Images └── 001 │ ├── test.JPEG │ ├── test2.JPEG │ └── test3.JPEG ├── LICENSE ├── Labels └── 001 │ ├── test.txt │ ├── test2.txt │ └── test3.txt ├── README.md ├── main.py └── screenshot.png /Examples/001/Cute-Dog-Wallpaper.JPEG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/Examples/001/Cute-Dog-Wallpaper.JPEG -------------------------------------------------------------------------------- /Examples/001/dog.JPEG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/Examples/001/dog.JPEG -------------------------------------------------------------------------------- /Examples/001/dogs-wallpaper.JPEG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/Examples/001/dogs-wallpaper.JPEG -------------------------------------------------------------------------------- /Images/001/test.JPEG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/Images/001/test.JPEG -------------------------------------------------------------------------------- /Images/001/test2.JPEG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/Images/001/test2.JPEG -------------------------------------------------------------------------------- /Images/001/test3.JPEG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/Images/001/test3.JPEG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shi Qiu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Labels/001/test.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 80 8 172 111 3 | -------------------------------------------------------------------------------- /Labels/001/test2.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 129 42 389 235 3 | -------------------------------------------------------------------------------- /Labels/001/test3.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 74 73 171 170 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BBox-Label-Tool 2 | =============== 3 | 4 | A simple tool for labeling object bounding boxes in images, implemented with Python Tkinter. 5 | 6 | **Updates:** 7 | - 2017.5.21 Check out the ```multi-class``` branch for a multi-class version implemented by @jxgu1016 8 | 9 | **Screenshot:** 10 | ![Label Tool](./screenshot.png) 11 | 12 | Data Organization 13 | ----------------- 14 | LabelTool 15 | | 16 | |--main.py *# source code for the tool* 17 | | 18 | |--Images/ *# direcotry containing the images to be labeled* 19 | | 20 | |--Labels/ *# direcotry for the labeling results* 21 | | 22 | |--Examples/ *# direcotry for the example bboxes* 23 | 24 | Environment 25 | ---------- 26 | - python 2.7 27 | - python PIL (Pillow) 28 | 29 | Run 30 | ------- 31 | $ python main.py 32 | 33 | Usage 34 | ----- 35 | 0. The current tool requires that **the images to be labeled reside in /Images/001, /Images/002, etc. You will need to modify the code if you want to label images elsewhere**. 36 | 1. Input a folder number (e.g, 1, 2, 5...), and click `Load`. The images in the folder, along with a few example results will be loaded. 37 | 2. To create a new bounding box, left-click to select the first vertex. Moving the mouse to draw a rectangle, and left-click again to select the second vertex. 38 | - To cancel the bounding box while drawing, just press ``. 39 | - To delete a existing bounding box, select it from the listbox, and click `Delete`. 40 | - To delete all existing bounding boxes in the image, simply click `ClearAll`. 41 | 3. After finishing one image, click `Next` to advance. Likewise, click `Prev` to reverse. Or, input an image id and click `Go` to navigate to the speficied image. 42 | - Be sure to click `Next` after finishing a image, or the result won't be saved. 43 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: Object bounding box label tool 3 | # Purpose: Label object bboxes for ImageNet Detection data 4 | # Author: Qiushi 5 | # Created: 06/06/2014 6 | 7 | # 8 | #------------------------------------------------------------------------------- 9 | from __future__ import division 10 | from Tkinter import * 11 | import tkMessageBox 12 | from PIL import Image, ImageTk 13 | import os 14 | import glob 15 | import random 16 | 17 | # colors for the bboxes 18 | COLORS = ['red', 'blue', 'yellow', 'pink', 'cyan', 'green', 'black'] 19 | # image sizes for the examples 20 | SIZE = 256, 256 21 | 22 | class LabelTool(): 23 | def __init__(self, master): 24 | # set up the main frame 25 | self.parent = master 26 | self.parent.title("LabelTool") 27 | self.frame = Frame(self.parent) 28 | self.frame.pack(fill=BOTH, expand=1) 29 | self.parent.resizable(width = FALSE, height = FALSE) 30 | 31 | # initialize global state 32 | self.imageDir = '' 33 | self.imageList= [] 34 | self.egDir = '' 35 | self.egList = [] 36 | self.outDir = '' 37 | self.cur = 0 38 | self.total = 0 39 | self.category = 0 40 | self.imagename = '' 41 | self.labelfilename = '' 42 | self.tkimg = None 43 | 44 | # initialize mouse state 45 | self.STATE = {} 46 | self.STATE['click'] = 0 47 | self.STATE['x'], self.STATE['y'] = 0, 0 48 | 49 | # reference to bbox 50 | self.bboxIdList = [] 51 | self.bboxId = None 52 | self.bboxList = [] 53 | self.hl = None 54 | self.vl = None 55 | 56 | # ----------------- GUI stuff --------------------- 57 | # dir entry & load 58 | self.label = Label(self.frame, text = "Image Dir:") 59 | self.label.grid(row = 0, column = 0, sticky = E) 60 | self.entry = Entry(self.frame) 61 | self.entry.grid(row = 0, column = 1, sticky = W+E) 62 | self.ldBtn = Button(self.frame, text = "Load", command = self.loadDir) 63 | self.ldBtn.grid(row = 0, column = 2, sticky = W+E) 64 | 65 | # main panel for labeling 66 | self.mainPanel = Canvas(self.frame, cursor='tcross') 67 | self.mainPanel.bind("", self.mouseClick) 68 | self.mainPanel.bind("", self.mouseMove) 69 | self.parent.bind("", self.cancelBBox) # press to cancel current bbox 70 | self.parent.bind("s", self.cancelBBox) 71 | self.parent.bind("a", self.prevImage) # press 'a' to go backforward 72 | self.parent.bind("d", self.nextImage) # press 'd' to go forward 73 | self.mainPanel.grid(row = 1, column = 1, rowspan = 4, sticky = W+N) 74 | 75 | # showing bbox info & delete bbox 76 | self.lb1 = Label(self.frame, text = 'Bounding boxes:') 77 | self.lb1.grid(row = 1, column = 2, sticky = W+N) 78 | self.listbox = Listbox(self.frame, width = 22, height = 12) 79 | self.listbox.grid(row = 2, column = 2, sticky = N) 80 | self.btnDel = Button(self.frame, text = 'Delete', command = self.delBBox) 81 | self.btnDel.grid(row = 3, column = 2, sticky = W+E+N) 82 | self.btnClear = Button(self.frame, text = 'ClearAll', command = self.clearBBox) 83 | self.btnClear.grid(row = 4, column = 2, sticky = W+E+N) 84 | 85 | # control panel for image navigation 86 | self.ctrPanel = Frame(self.frame) 87 | self.ctrPanel.grid(row = 5, column = 1, columnspan = 2, sticky = W+E) 88 | self.prevBtn = Button(self.ctrPanel, text='<< Prev', width = 10, command = self.prevImage) 89 | self.prevBtn.pack(side = LEFT, padx = 5, pady = 3) 90 | self.nextBtn = Button(self.ctrPanel, text='Next >>', width = 10, command = self.nextImage) 91 | self.nextBtn.pack(side = LEFT, padx = 5, pady = 3) 92 | self.progLabel = Label(self.ctrPanel, text = "Progress: / ") 93 | self.progLabel.pack(side = LEFT, padx = 5) 94 | self.tmpLabel = Label(self.ctrPanel, text = "Go to Image No.") 95 | self.tmpLabel.pack(side = LEFT, padx = 5) 96 | self.idxEntry = Entry(self.ctrPanel, width = 5) 97 | self.idxEntry.pack(side = LEFT) 98 | self.goBtn = Button(self.ctrPanel, text = 'Go', command = self.gotoImage) 99 | self.goBtn.pack(side = LEFT) 100 | 101 | # example pannel for illustration 102 | self.egPanel = Frame(self.frame, border = 10) 103 | self.egPanel.grid(row = 1, column = 0, rowspan = 5, sticky = N) 104 | self.tmpLabel2 = Label(self.egPanel, text = "Examples:") 105 | self.tmpLabel2.pack(side = TOP, pady = 5) 106 | self.egLabels = [] 107 | for i in range(3): 108 | self.egLabels.append(Label(self.egPanel)) 109 | self.egLabels[-1].pack(side = TOP) 110 | 111 | # display mouse position 112 | self.disp = Label(self.ctrPanel, text='') 113 | self.disp.pack(side = RIGHT) 114 | 115 | self.frame.columnconfigure(1, weight = 1) 116 | self.frame.rowconfigure(4, weight = 1) 117 | 118 | # for debugging 119 | ## self.setImage() 120 | ## self.loadDir() 121 | 122 | def loadDir(self, dbg = False): 123 | if not dbg: 124 | s = self.entry.get() 125 | self.parent.focus() 126 | self.category = int(s) 127 | else: 128 | s = r'D:\workspace\python\labelGUI' 129 | ## if not os.path.isdir(s): 130 | ## tkMessageBox.showerror("Error!", message = "The specified dir doesn't exist!") 131 | ## return 132 | # get image list 133 | self.imageDir = os.path.join(r'./Images', '%03d' %(self.category)) 134 | self.imageList = glob.glob(os.path.join(self.imageDir, '*.JPEG')) 135 | if len(self.imageList) == 0: 136 | print 'No .JPEG images found in the specified dir!' 137 | return 138 | 139 | # default to the 1st image in the collection 140 | self.cur = 1 141 | self.total = len(self.imageList) 142 | 143 | # set up output dir 144 | self.outDir = os.path.join(r'./Labels', '%03d' %(self.category)) 145 | if not os.path.exists(self.outDir): 146 | os.mkdir(self.outDir) 147 | 148 | # load example bboxes 149 | self.egDir = os.path.join(r'./Examples', '%03d' %(self.category)) 150 | if not os.path.exists(self.egDir): 151 | return 152 | filelist = glob.glob(os.path.join(self.egDir, '*.JPEG')) 153 | self.tmp = [] 154 | self.egList = [] 155 | random.shuffle(filelist) 156 | for (i, f) in enumerate(filelist): 157 | if i == 3: 158 | break 159 | im = Image.open(f) 160 | r = min(SIZE[0] / im.size[0], SIZE[1] / im.size[1]) 161 | new_size = int(r * im.size[0]), int(r * im.size[1]) 162 | self.tmp.append(im.resize(new_size, Image.ANTIALIAS)) 163 | self.egList.append(ImageTk.PhotoImage(self.tmp[-1])) 164 | self.egLabels[i].config(image = self.egList[-1], width = SIZE[0], height = SIZE[1]) 165 | 166 | self.loadImage() 167 | print '%d images loaded from %s' %(self.total, s) 168 | 169 | def loadImage(self): 170 | # load image 171 | imagepath = self.imageList[self.cur - 1] 172 | self.img = Image.open(imagepath) 173 | self.tkimg = ImageTk.PhotoImage(self.img) 174 | self.mainPanel.config(width = max(self.tkimg.width(), 400), height = max(self.tkimg.height(), 400)) 175 | self.mainPanel.create_image(0, 0, image = self.tkimg, anchor=NW) 176 | self.progLabel.config(text = "%04d/%04d" %(self.cur, self.total)) 177 | 178 | # load labels 179 | self.clearBBox() 180 | self.imagename = os.path.split(imagepath)[-1].split('.')[0] 181 | labelname = self.imagename + '.txt' 182 | self.labelfilename = os.path.join(self.outDir, labelname) 183 | bbox_cnt = 0 184 | if os.path.exists(self.labelfilename): 185 | with open(self.labelfilename) as f: 186 | for (i, line) in enumerate(f): 187 | if i == 0: 188 | bbox_cnt = int(line.strip()) 189 | continue 190 | tmp = [int(t.strip()) for t in line.split()] 191 | ## print tmp 192 | self.bboxList.append(tuple(tmp)) 193 | tmpId = self.mainPanel.create_rectangle(tmp[0], tmp[1], \ 194 | tmp[2], tmp[3], \ 195 | width = 2, \ 196 | outline = COLORS[(len(self.bboxList)-1) % len(COLORS)]) 197 | self.bboxIdList.append(tmpId) 198 | self.listbox.insert(END, '(%d, %d) -> (%d, %d)' %(tmp[0], tmp[1], tmp[2], tmp[3])) 199 | self.listbox.itemconfig(len(self.bboxIdList) - 1, fg = COLORS[(len(self.bboxIdList) - 1) % len(COLORS)]) 200 | 201 | def saveImage(self): 202 | with open(self.labelfilename, 'w') as f: 203 | f.write('%d\n' %len(self.bboxList)) 204 | for bbox in self.bboxList: 205 | f.write(' '.join(map(str, bbox)) + '\n') 206 | print 'Image No. %d saved' %(self.cur) 207 | 208 | 209 | def mouseClick(self, event): 210 | if self.STATE['click'] == 0: 211 | self.STATE['x'], self.STATE['y'] = event.x, event.y 212 | else: 213 | x1, x2 = min(self.STATE['x'], event.x), max(self.STATE['x'], event.x) 214 | y1, y2 = min(self.STATE['y'], event.y), max(self.STATE['y'], event.y) 215 | self.bboxList.append((x1, y1, x2, y2)) 216 | self.bboxIdList.append(self.bboxId) 217 | self.bboxId = None 218 | self.listbox.insert(END, '(%d, %d) -> (%d, %d)' %(x1, y1, x2, y2)) 219 | self.listbox.itemconfig(len(self.bboxIdList) - 1, fg = COLORS[(len(self.bboxIdList) - 1) % len(COLORS)]) 220 | self.STATE['click'] = 1 - self.STATE['click'] 221 | 222 | def mouseMove(self, event): 223 | self.disp.config(text = 'x: %d, y: %d' %(event.x, event.y)) 224 | if self.tkimg: 225 | if self.hl: 226 | self.mainPanel.delete(self.hl) 227 | self.hl = self.mainPanel.create_line(0, event.y, self.tkimg.width(), event.y, width = 2) 228 | if self.vl: 229 | self.mainPanel.delete(self.vl) 230 | self.vl = self.mainPanel.create_line(event.x, 0, event.x, self.tkimg.height(), width = 2) 231 | if 1 == self.STATE['click']: 232 | if self.bboxId: 233 | self.mainPanel.delete(self.bboxId) 234 | self.bboxId = self.mainPanel.create_rectangle(self.STATE['x'], self.STATE['y'], \ 235 | event.x, event.y, \ 236 | width = 2, \ 237 | outline = COLORS[len(self.bboxList) % len(COLORS)]) 238 | 239 | def cancelBBox(self, event): 240 | if 1 == self.STATE['click']: 241 | if self.bboxId: 242 | self.mainPanel.delete(self.bboxId) 243 | self.bboxId = None 244 | self.STATE['click'] = 0 245 | 246 | def delBBox(self): 247 | sel = self.listbox.curselection() 248 | if len(sel) != 1 : 249 | return 250 | idx = int(sel[0]) 251 | self.mainPanel.delete(self.bboxIdList[idx]) 252 | self.bboxIdList.pop(idx) 253 | self.bboxList.pop(idx) 254 | self.listbox.delete(idx) 255 | 256 | def clearBBox(self): 257 | for idx in range(len(self.bboxIdList)): 258 | self.mainPanel.delete(self.bboxIdList[idx]) 259 | self.listbox.delete(0, len(self.bboxList)) 260 | self.bboxIdList = [] 261 | self.bboxList = [] 262 | 263 | def prevImage(self, event = None): 264 | self.saveImage() 265 | if self.cur > 1: 266 | self.cur -= 1 267 | self.loadImage() 268 | 269 | def nextImage(self, event = None): 270 | self.saveImage() 271 | if self.cur < self.total: 272 | self.cur += 1 273 | self.loadImage() 274 | 275 | def gotoImage(self): 276 | idx = int(self.idxEntry.get()) 277 | if 1 <= idx and idx <= self.total: 278 | self.saveImage() 279 | self.cur = idx 280 | self.loadImage() 281 | 282 | ## def setImage(self, imagepath = r'test2.png'): 283 | ## self.img = Image.open(imagepath) 284 | ## self.tkimg = ImageTk.PhotoImage(self.img) 285 | ## self.mainPanel.config(width = self.tkimg.width()) 286 | ## self.mainPanel.config(height = self.tkimg.height()) 287 | ## self.mainPanel.create_image(0, 0, image = self.tkimg, anchor=NW) 288 | 289 | if __name__ == '__main__': 290 | root = Tk() 291 | tool = LabelTool(root) 292 | root.resizable(width = True, height = True) 293 | root.mainloop() 294 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzledqs/BBox-Label-Tool/80b9b8d668ba1aa4a8a593a53995094a27e17033/screenshot.png --------------------------------------------------------------------------------