├── .gitignore ├── LICENSE ├── README.md ├── bbox_annotator.coffee ├── demo.html ├── example.jpg └── mturk.html /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | bbox_annotator.js 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Kota Yamaguchi 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bounding box annotator 2 | ====================== 3 | 4 | A bounding box annotation tool written in CoffeeScript/JavaScript. 5 | There is an online demo at https://kyamagu.github.io/bbox-annotator/demo.html 6 | 7 | Contents 8 | -------- 9 | 10 | bbox_annotator.coffee Main code. 11 | demo.html Example to host on a private server. 12 | mturk.html Example to use in Amazon MTurk. 13 | README.md This documentation. 14 | 15 | Be sure to unescape `×` with `×` when directly editing an MTurk template on 16 | the Amazon requester UI. 17 | 18 | Compile 19 | ------- 20 | 21 | Compile the coffee script to get a javascript. 22 | 23 | coffee -c bbox_annotator.coffee 24 | 25 | Or, go to http://coffeescript.org/ and compile the coffeescript. 26 | 27 | Usage 28 | ----- 29 | 30 | Load the compiled javascript codes inside HTML. 31 | 32 | 33 | 34 | Embed a block element for annotation tool and a form element in HTML. 35 | 36 |
37 | 38 | 39 | Then, attach a piece of script to launch the annotation tool. The annotator 40 | takes a callback function on change of the annotation data. Use JSON format to 41 | display the value. 42 | 43 | 53 | 54 | The returned annotation is an array of objects. Here is an example. 55 | 56 | [ 57 | { 58 | "top": 0, 59 | "left": 0, 60 | "width": 100, 61 | "height": 100, 62 | "label", "object" 63 | } 64 | ] 65 | 66 | Check the attached `demo.html` and `mturk.html` for how to use. 67 | 68 | Options 69 | ------- 70 | 71 | The `BBoxAnnotator` object takes options to change its behavior. 72 | 73 | * `id`: CSS selector for the annotator element. Default is `"#bbox_annotator"`. 74 | * `input_method`: One of the "text", "select", and "fixed". This will change 75 | how to input associated annotation text. 76 | * `labels`: A string, or an array of strings. Set of labels used for suggestion 77 | in "text" or "select" input. When fixed, only the first element is chosen. 78 | * `width`: Width of the image. The default is the original width of the image. 79 | * `height`: Height of the image. The default is the original height of the 80 | image. 81 | * `show_label`: Flag to display a label in the box. Default is true for "text" 82 | and "select" input, and false for "fixed" input. 83 | * `border_width`: Size of border around the image. Default is 2. Leaving it 0 84 | can make it hard to start annotation around the image border. 85 | * `multiple`: Defines if multiple boxes could be selected. Default is `true`. 86 | * `guide`: Enable vertical and horizontal guide lines. Specify `true`, or 87 | object that specifies color like `{'color': '#fff'}`. 88 | 89 | License 90 | ------- 91 | 92 | The code may be redistributed under BSD license. 93 | -------------------------------------------------------------------------------- /bbox_annotator.coffee: -------------------------------------------------------------------------------- 1 | # Use coffee-script compiler to obtain a javascript file. 2 | # 3 | # coffee -c bbox_annotator.coffee 4 | # 5 | # See http://coffeescript.org/ 6 | 7 | # BBox selection window. 8 | class BBoxSelector 9 | # Initializes selector in the image frame. 10 | constructor: (image_frame, options) -> 11 | options ?= {} 12 | options.input_method ||= "text" 13 | @image_frame = image_frame 14 | @border_width = options.border_width || 2 15 | @selector = $('
') 16 | @selector.css 17 | "border": (@border_width) + "px dotted rgb(127,255,127)", 18 | "position": "absolute" 19 | @image_frame.append @selector 20 | @selector.css 21 | "border-width": @border_width 22 | @selector.hide() 23 | this.create_label_box(options) 24 | 25 | # Initializes a label input box. 26 | create_label_box: (options) -> 27 | options.labels ||= ["object"] 28 | @label_box = $('
') 29 | @label_box.css 30 | "position": "absolute" 31 | @image_frame.append @label_box 32 | switch options.input_method 33 | when 'select' 34 | options.labels = [options.labels] if typeof options.labels == "string" 35 | @label_input = $('') 36 | @label_box.append @label_input 37 | @label_input.append($('')) 38 | for label in options.labels 39 | @label_input.append '' 41 | @label_input.change (e) -> this.blur() 42 | when 'text' 43 | options.labels = [options.labels] if typeof options.labels == "string" 44 | @label_input = $('') 46 | @label_box.append @label_input 47 | @label_input.autocomplete 48 | source: options.labels || [''] 49 | autoFocus: true 50 | when 'fixed' 51 | options.labels = options.labels[0] if $.isArray options.labels 52 | @label_input = $('') 53 | @label_box.append @label_input 54 | @label_input.val(options.labels) 55 | else 56 | throw 'Invalid label_input parameter: ' + options.input_method 57 | @label_box.hide() 58 | 59 | # Crop x and y to the image size. 60 | crop: (pageX, pageY) -> 61 | point = 62 | x: Math.min(Math.max(Math.round(pageX - @image_frame.offset().left), 0), 63 | Math.round(@image_frame.width()-1)) 64 | y: Math.min(Math.max(Math.round(pageY - @image_frame.offset().top), 0), 65 | Math.round(@image_frame.height()-1)) 66 | 67 | # When a new selection is made. 68 | start: (pageX, pageY) -> 69 | @pointer = this.crop(pageX, pageY) 70 | @offset = @pointer 71 | this.refresh() 72 | @selector.show() 73 | $('body').css('cursor', 'crosshair') 74 | document.onselectstart = () -> 75 | false 76 | 77 | # When a selection updates. 78 | update_rectangle: (pageX, pageY) -> 79 | @pointer = this.crop(pageX, pageY) 80 | this.refresh() 81 | 82 | # When starting to input label. 83 | input_label: (options) -> 84 | $('body').css('cursor', 'default') 85 | document.onselectstart = () -> 86 | true 87 | @label_box.show() 88 | @label_input.focus() 89 | 90 | # Finish and return the annotation. 91 | finish: (options) -> 92 | @label_box.hide() 93 | @selector.hide() 94 | data = this.rectangle() 95 | data.label = $.trim(@label_input.val().toLowerCase()) 96 | @label_input.val('') unless options.input_method == 'fixed' 97 | data 98 | 99 | # Get a rectangle. 100 | rectangle: () -> 101 | x1 = Math.min(@offset.x, @pointer.x) 102 | y1 = Math.min(@offset.y, @pointer.y) 103 | x2 = Math.max(@offset.x, @pointer.x) 104 | y2 = Math.max(@offset.y, @pointer.y) 105 | rect = 106 | left: x1 107 | top: y1 108 | width: x2 - x1 + 1 109 | height: y2 - y1 + 1 110 | 111 | # Update css of the box. 112 | refresh: () -> 113 | rect = this.rectangle() 114 | @selector.css( 115 | left: (rect.left - @border_width) + 'px' 116 | top: (rect.top - @border_width) + 'px' 117 | width: rect.width + 'px' 118 | height: rect.height + 'px' 119 | ) 120 | @label_box.css( 121 | left: (rect.left - @border_width) + 'px' 122 | top: (rect.top + rect.height + @border_width) + 'px' 123 | ) 124 | 125 | # Return input element. 126 | get_input_element: () -> 127 | @label_input 128 | 129 | # Annotator object definition. 130 | class @BBoxAnnotator 131 | # Initialize the annotator layout and events. 132 | constructor: (options) -> 133 | annotator = this 134 | @annotator_element = $(options.id || "#bbox_annotator") 135 | @border_width = options.border_width || 2 136 | @show_label = options.show_label || (options.input_method != "fixed") 137 | 138 | if options.multiple? 139 | @multiple = options.multiple 140 | else 141 | @multiple = true 142 | 143 | @image_frame = $('
') 144 | @annotator_element.append @image_frame 145 | annotator.initialize_guide(options.guide) if options.guide 146 | 147 | image_element = new Image() 148 | image_element.src = options.url 149 | image_element.onload = () -> 150 | options.width ||= image_element.width 151 | options.height ||= image_element.height 152 | annotator.annotator_element.css 153 | "width": (options.width + annotator.border_width) + 'px', 154 | "height": (options.height + annotator.border_width) + 'px', 155 | "padding-left": (annotator.border_width / 2) + 'px', 156 | "padding-top": (annotator.border_width / 2) + 'px', 157 | "cursor": "crosshair", 158 | "overflow": "hidden" 159 | annotator.image_frame.css 160 | "background-image": "url('" + image_element.src + "')", 161 | "width": options.width + "px", 162 | "height": options.height + "px", 163 | "position": "relative" 164 | annotator.selector = new BBoxSelector(annotator.image_frame, options) 165 | annotator.initialize_events(options) 166 | image_element.onerror = () -> 167 | annotator.annotator_element.text "Invalid image URL: " + options.url 168 | @entries = [] 169 | @onchange = options.onchange 170 | 171 | # Initialize events. 172 | initialize_events: (options) -> 173 | status = 'free' 174 | @hit_menuitem = false 175 | annotator = this 176 | selector = annotator.selector 177 | @annotator_element.mousedown (e) -> 178 | unless annotator.hit_menuitem 179 | switch status 180 | when 'free', 'input' 181 | selector.get_input_element().blur() if status == 'input' 182 | if e.which == 1 # left button 183 | selector.start(e.pageX, e.pageY) 184 | status = 'hold' 185 | annotator.hit_menuitem = false 186 | true 187 | $(window).mousemove (e) -> 188 | switch status 189 | when 'hold' 190 | selector.update_rectangle(e.pageX, e.pageY) 191 | if annotator.guide_h 192 | offset = annotator.image_frame.offset() 193 | annotator.guide_h.css('top', Math.floor(e.pageY - offset.top) + 'px') 194 | annotator.guide_v.css('left', Math.floor(e.pageX - offset.left) + 'px') 195 | true 196 | $(window).mouseup (e) -> 197 | switch status 198 | when 'hold' 199 | selector.update_rectangle(e.pageX, e.pageY) 200 | selector.input_label(options) 201 | status = 'input' 202 | selector.get_input_element().blur() if options.input_method == 'fixed' 203 | true 204 | selector.get_input_element().blur (e) -> 205 | switch status 206 | when 'input' 207 | data = selector.finish(options) 208 | if data.label 209 | annotator.add_entry data 210 | annotator.onchange annotator.entries if annotator.onchange 211 | status = 'free' 212 | true 213 | selector.get_input_element().keypress (e) -> 214 | switch status 215 | when 'input' 216 | selector.get_input_element().blur() if e.which == 13 217 | e.which != 13 218 | selector.get_input_element().mousedown (e) -> 219 | annotator.hit_menuitem = true 220 | selector.get_input_element().mousemove (e) -> 221 | annotator.hit_menuitem = true 222 | selector.get_input_element().mouseup (e) -> 223 | annotator.hit_menuitem = true 224 | selector.get_input_element().parent().mousedown (e) -> 225 | annotator.hit_menuitem = true 226 | 227 | # Add a new entry. 228 | add_entry: (entry) -> 229 | unless @multiple 230 | @annotator_element.find(".annotated_bounding_box").detach() 231 | @entries.splice 0 232 | 233 | @entries.push entry 234 | box_element = $('
') 235 | box_element.appendTo(@image_frame).css 236 | "border": @border_width + "px solid rgb(127,255,127)", 237 | "position": "absolute", 238 | "top": (entry.top - @border_width) + "px", 239 | "left": (entry.left - @border_width) + "px", 240 | "width": entry.width + "px", 241 | "height": entry.height + "px", 242 | "color": "rgb(127,255,127)", 243 | "font-family": "monospace", 244 | "font-size": "small" 245 | close_button = $('
').appendTo(box_element).css 246 | "position": "absolute", 247 | "top": "-8px", 248 | "right": "-8px", 249 | "width": "16px", 250 | "height": "0", 251 | "padding": "16px 0 0 0", 252 | "overflow": "hidden", 253 | "color": "#fff", 254 | "background-color": "#030", 255 | "border": "2px solid #fff", 256 | "-moz-border-radius": "18px", 257 | "-webkit-border-radius": "18px", 258 | "border-radius": "18px", 259 | "cursor": "pointer", 260 | "-moz-user-select": "none", 261 | "-webkit-user-select": "none", 262 | "user-select": "none", 263 | "text-align": "center" 264 | $("
").appendTo(close_button).html('×').css 265 | "display": "block", 266 | "text-align": "center", 267 | "width": "16px", 268 | "position": "absolute", 269 | "top": "-2px", 270 | "left": "0", 271 | "font-size": "16px", 272 | "line-height": "16px", 273 | "font-family": '"Helvetica Neue", Consolas, Verdana, Tahoma, Calibri, ' + 274 | 'Helvetica, Menlo, "Droid Sans", sans-serif', 275 | text_box = $('
').appendTo(box_element).css 276 | "overflow": "hidden" 277 | text_box.text(entry.label) if @show_label 278 | annotator = this 279 | box_element.hover ((e) -> close_button.show()), ((e) -> close_button.hide()) 280 | close_button.mousedown (e) -> 281 | annotator.hit_menuitem = true 282 | close_button.click (e) -> 283 | clicked_box = close_button.parent(".annotated_bounding_box") 284 | index = clicked_box.prevAll(".annotated_bounding_box").length 285 | clicked_box.detach() 286 | annotator.entries.splice index, 1 287 | annotator.onchange annotator.entries 288 | close_button.hide() 289 | 290 | # Clear all entries. 291 | clear_all: (e) -> 292 | @annotator_element.find(".annotated_bounding_box").detach() 293 | this.entries.splice 0 294 | this.onchange this.entries 295 | 296 | # Add crosshair guide. 297 | initialize_guide: (options) -> 298 | @guide_h = $('
').appendTo(@image_frame).css 299 | "border": "1px dotted " + (options.color || '#000'), 300 | "height": "0", 301 | "width": "100%", 302 | "position": "absolute", 303 | "top": "0", 304 | "left": "0" 305 | @guide_v = $('
').appendTo(@image_frame).css 306 | "border": "1px dotted " + (options.color || '#000'), 307 | "height": "100%", 308 | "width": "0", 309 | "position": "absolute", 310 | "top": "0", 311 | "left": "0" 312 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bounding box annotator demo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Bounding box annotator demo

12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyamagu/bbox-annotator/5f31537e436c31d5618dc1c521d2a4344f11809e/example.jpg -------------------------------------------------------------------------------- /mturk.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 |

Annotate objects in the picture

9 |

10 | Draw a rectangle over an object with mouse. Then, name that object. 11 |

12 |
13 |
14 | 15 |
16 |

17 | 18 | 19 |

20 |
21 |
22 | 23 | 471 | --------------------------------------------------------------------------------