I\'d like to elaborate on the accepted answer to this question.
I\'m looking at improving the minimal shiny app below (extracted from the accepted answer) with the follo
This solution uses kyamagu's bbox_annotator and is based on demo.html. I'm not familiar with JS, so it's not the prettiest. Limitations are:
ui.R
# Adapted from https://github.com/kyamagu/bbox-annotator/
# Edited original JS to add color_list as an option
# ...should be the same length as labels
# ...and controls the color of the rectangle
# ...will probably be broken for input_method = "fixed" or "text"
# Also added color as a value in each rectangle entry
js <- '
$(document).ready(function() {
// define options to pass to bounding box constructor
var options = {
url: "https://www.r-project.org/logo/Rlogo.svg",
input_method: "select",
labels: [""],
color_list: [""],
onchange: function(entries) {
Shiny.onInputChange("rectCoord", JSON.stringify(entries, null, " "));
}
};
// Initialize the bounding-box annotator.
var annotator = new BBoxAnnotator(options);
// Initialize the reset button.
$("#reset_button").click(function(e) {
annotator.clear_all();
})
// define function to reset the bbox
// ...upon choosing new label category or new url
function reset_bbox(options) {
document.getElementById("bbox_annotator").setAttribute("style", "display:inline-block");
$(".image_frame").remove();
annotator = new BBoxAnnotator(options);
}
// update image url from shiny
Shiny.addCustomMessageHandler("change-img-url", function(url) {
options.url = url;
options.width = null;
options.height = null;
reset_bbox(options);
});
// update colors and categories from shiny
Shiny.addCustomMessageHandler("update-category-list", function(vals) {
options.labels = Object.values(vals);
options.color_list = Object.keys(vals);
reset_bbox(options);
});
// redraw rectangles based on list of entries
Shiny.addCustomMessageHandler("redraw-rects", function(vals) {
var arr = JSON.parse(vals);
arr.forEach(function(rect){
annotator.add_entry(rect);
});
if (annotator.onchange) {
annotator.onchange(annotator.entries);
}
});
});
'
ui <- fluidPage(
tags$head(tags$script(HTML(js)),
tags$head(
tags$script(src = "bbox_annotation.js")
)),
titlePanel("Bounding box annotator demo"),
sidebarLayout(
sidebarPanel(
selectInput(
"img_url",
"URLs",
c(
"https://www.r-project.org/logo/Rlogo.svg",
"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
)
),
selectInput("category_type", "Label Category", c("animals", "fruits")),
div(HTML(
'<input id="reset_button" type="reset" />'
)),
HTML(
'<input id="annotation_data" name="annotation_data" type="hidden" />'
),
hr(),
h4("Entries"),
verbatimTextOutput("rectCoordOutput")
),
mainPanel(div(id = "bbox_annotator", style = "display:inline-block"))
)
)
server.R
server <- function(input, output, session) {
# user choices
output$rectCoordOutput <- renderPrint({
if(!is.null(input$rectCoord)) {
as.data.frame(jsonlite::fromJSON(input$rectCoord))
}
})
# send chosen URL from shiny to JS
observeEvent(input$img_url, {
session$sendCustomMessage("change-img-url", input$img_url)
})
# send chosen category list from shiny to JS
observeEvent(input$category_type, {
vals <- switch(input$category_type,
fruits = list("yellow" = "banana",
"orange" = "pineapple",
"pink" = "grapefruit"),
animals = list("grey" = "raccoon",
"brown" = "dog",
"tan" = "cat")
)
# update category list
session$sendCustomMessage("update-category-list", vals)
# redraw rectangles
session$sendCustomMessage("redraw-rects", input$rectCoord)
})
}
www/bbox_annotation.js
// Generated by CoffeeScript 2.5.0
(function() {
// https://github.com/kyamagu/bbox-annotator/blob/master/bbox_annotator.coffee
// Use coffee-script compiler to obtain a javascript file.
// coffee -c bbox_annotator.coffee
// See http://coffeescript.org/
// BBox selection window.
var BBoxSelector;
BBoxSelector = class BBoxSelector {
// Initializes selector in the image frame.
constructor(image_frame, options) {
if (options == null) {
options = {};
}
options.input_method || (options.input_method = "text");
this.image_frame = image_frame;
this.border_width = options.border_width || 2;
this.selector = $('<div class="bbox_selector"></div>');
this.selector.css({
// rectangle color when dragging
"border": this.border_width + "px dotted rgb(127,255,127)",
"position": "absolute"
});
this.image_frame.append(this.selector);
this.selector.css({
"border-width": this.border_width
});
this.selector.hide();
this.create_label_box(options);
}
// Initializes a label input box.
create_label_box(options) {
var i, label, len, ref;
options.labels || (options.labels = ["object"]);
this.label_box = $('<div class="label_box" style="z-index: 1000"></div>');
this.label_box.css({
"position": "absolute"
});
this.image_frame.append(this.label_box);
switch (options.input_method) {
case 'select':
if (typeof options.labels === "string") {
options.labels = [options.labels];
}
this.label_input = $('<select class="label_input" name="label"></select>');
this.label_box.append(this.label_input);
this.label_input.append($('<option value>choose an item</option>'));
ref = options.labels;
for (i = 0, len = ref.length; i < len; i++) {
label = ref[i];
this.label_input.append('<option value="' + label + '">' + label + '</option>');
}
this.label_input.change(function(e) {
return this.blur();
});
break;
case 'text':
if (typeof options.labels === "string") {
options.labels = [options.labels];
}
this.label_input = $('<input class="label_input" name="label" ' + 'type="text" value>');
this.label_box.append(this.label_input);
this.label_input.autocomplete({
source: options.labels || [''],
autoFocus: true
});
break;
case 'fixed':
if ($.isArray(options.labels)) {
options.labels = options.labels[0];
}
this.label_input = $('<input class="label_input" name="label" type="text">');
this.label_box.append(this.label_input);
this.label_input.val(options.labels);
break;
default:
throw 'Invalid label_input parameter: ' + options.input_method;
}
return this.label_box.hide();
}
// Crop x and y to the image size.
crop(pageX, pageY) {
var point;
return point = {
x: Math.min(Math.max(Math.round(pageX - this.image_frame.offset().left), 0), Math.round(this.image_frame.width() - 1)),
y: Math.min(Math.max(Math.round(pageY - this.image_frame.offset().top), 0), Math.round(this.image_frame.height() - 1))
};
}
// When a new selection is made.
start(pageX, pageY) {
this.pointer = this.crop(pageX, pageY);
this.offset = this.pointer;
this.refresh();
this.selector.show();
$('body').css('cursor', 'crosshair');
return document.onselectstart = function() {
return false;
};
}
// When a selection updates.
update_rectangle(pageX, pageY) {
this.pointer = this.crop(pageX, pageY);
return this.refresh();
}
// When starting to input label.
input_label(options) {
$('body').css('cursor', 'default');
document.onselectstart = function() {
return true;
};
this.label_box.show();
return this.label_input.focus();
}
// Finish and return the annotation.
finish(options) {
var data;
this.label_box.hide();
this.selector.hide();
data = this.rectangle();
data.label = $.trim(this.label_input.val().toLowerCase());
if (options.input_method !== 'fixed') {
this.label_input.val('');
}
return data;
}
// Get a rectangle.
rectangle() {
var rect, x1, x2, y1, y2;
x1 = Math.min(this.offset.x, this.pointer.x);
y1 = Math.min(this.offset.y, this.pointer.y);
x2 = Math.max(this.offset.x, this.pointer.x);
y2 = Math.max(this.offset.y, this.pointer.y);
return rect = {
left: x1,
top: y1,
width: x2 - x1 + 1,
height: y2 - y1 + 1
};
}
// Update css of the box.
refresh() {
var rect;
rect = this.rectangle();
this.selector.css({
left: (rect.left - this.border_width) + 'px',
top: (rect.top - this.border_width) + 'px',
width: rect.width + 'px',
height: rect.height + 'px'
});
return this.label_box.css({
left: (rect.left - this.border_width) + 'px',
top: (rect.top + rect.height + this.border_width) + 'px'
});
}
// Return input element.
get_input_element() {
return this.label_input;
}
};
// Annotator object definition.
this.BBoxAnnotator = class BBoxAnnotator {
// Initialize the annotator layout and events.
constructor(options) {
var annotator, image_element;
annotator = this;
this.annotator_element = $(options.id || "#bbox_annotator");
// allow us to access colors and labels in future steps
this.color_list = options.color_list;
this.label_list = options.labels;
this.border_width = options.border_width || 2;
this.show_label = options.show_label || (options.input_method !== "fixed");
if (options.multiple != null) {
this.multiple = options.multiple;
} else {
this.multiple = true;
}
this.image_frame = $('<div class="image_frame"></div>');
this.annotator_element.append(this.image_frame);
if (options.guide) {
annotator.initialize_guide(options.guide);
}
image_element = new Image();
image_element.src = options.url;
image_element.onload = function() {
options.width || (options.width = image_element.width);
options.height || (options.height = image_element.height);
annotator.annotator_element.css({
"width": (options.width + annotator.border_width) + 'px',
"height": (options.height + annotator.border_width) + 'px',
"padding-left": (annotator.border_width / 2) + 'px',
"padding-top": (annotator.border_width / 2) + 'px',
"cursor": "crosshair",
"overflow": "hidden"
});
annotator.image_frame.css({
"background-image": "url('" + image_element.src + "')",
"width": options.width + "px",
"height": options.height + "px",
"position": "relative"
});
annotator.selector = new BBoxSelector(annotator.image_frame, options);
return annotator.initialize_events(options);
};
image_element.onerror = function() {
return annotator.annotator_element.text("Invalid image URL: " + options.url);
};
this.entries = [];
this.onchange = options.onchange;
}
// Initialize events.
initialize_events(options) {
var annotator, selector, status;
status = 'free';
this.hit_menuitem = false;
annotator = this;
selector = annotator.selector;
this.annotator_element.mousedown(function(e) {
if (!annotator.hit_menuitem) {
switch (status) {
case 'free':
case 'input':
if (status === 'input') {
selector.get_input_element().blur();
}
if (e.which === 1) { // left button
selector.start(e.pageX, e.pageY);
status = 'hold';
}
}
}
annotator.hit_menuitem = false;
return true;
});
$(window).mousemove(function(e) {
var offset;
switch (status) {
case 'hold':
selector.update_rectangle(e.pageX, e.pageY);
}
if (annotator.guide_h) {
offset = annotator.image_frame.offset();
annotator.guide_h.css('top', Math.floor(e.pageY - offset.top) + 'px');
annotator.guide_v.css('left', Math.floor(e.pageX - offset.left) + 'px');
}
return true;
});
$(window).mouseup(function(e) {
switch (status) {
case 'hold':
selector.update_rectangle(e.pageX, e.pageY);
selector.input_label(options);
status = 'input';
if (options.input_method === 'fixed') {
selector.get_input_element().blur();
}
}
return true;
});
selector.get_input_element().blur(function(e) {
var data;
switch (status) {
case 'input':
data = selector.finish(options);
if (data.label) {
// store color with the entry
// ...so we can redraw the rectangle upon changing label category
data.color = annotator.color_list[annotator.label_list.indexOf(data.label)];
annotator.add_entry(data);
if (annotator.onchange) {
annotator.onchange(annotator.entries);
}
}
status = 'free';
}
return true;
});
selector.get_input_element().keypress(function(e) {
switch (status) {
case 'input':
if (e.which === 13) {
selector.get_input_element().blur();
}
}
return e.which !== 13;
});
selector.get_input_element().mousedown(function(e) {
return annotator.hit_menuitem = true;
});
selector.get_input_element().mousemove(function(e) {
return annotator.hit_menuitem = true;
});
selector.get_input_element().mouseup(function(e) {
return annotator.hit_menuitem = true;
});
return selector.get_input_element().parent().mousedown(function(e) {
return annotator.hit_menuitem = true;
});
}
// Add a new entry.
add_entry(entry) {
var annotator, box_element, close_button, text_box;
if (!this.multiple) {
this.annotator_element.find(".annotated_bounding_box").detach();
this.entries.splice(0);
}
this.entries.push(entry);
box_element = $('<div class="annotated_bounding_box"></div>');
box_element.appendTo(this.image_frame).css({
// rectangle color -- when stopped dragging
"border": this.border_width + "px solid " + entry.color,
"position": "absolute",
"top": (entry.top - this.border_width) + "px",
"left": (entry.left - this.border_width) + "px",
"width": entry.width + "px",
"height": entry.height + "px",
// text color when stopped dragging
"color": entry.color,
"font-family": "monospace",
"font-size": "small"
});
close_button = $('<div></div>').appendTo(box_element).css({
"position": "absolute",
"top": "-8px",
"right": "-8px",
"width": "16px",
"height": "0",
"padding": "16px 0 0 0",
"overflow": "hidden",
"color": "#fff",
"background-color": "#030",
"border": "2px solid #fff",
"-moz-border-radius": "18px",
"-webkit-border-radius": "18px",
"border-radius": "18px",
"cursor": "pointer",
"-moz-user-select": "none",
"-webkit-user-select": "none",
"user-select": "none",
"text-align": "center"
});
$("<div></div>").appendTo(close_button).html('×').css({
"display": "block",
"text-align": "center",
"width": "16px",
"position": "absolute",
"top": "-2px",
"left": "0",
"font-size": "16px",
"line-height": "16px",
"font-family": '"Helvetica Neue", Consolas, Verdana, Tahoma, Calibri, ' + 'Helvetica, Menlo, "Droid Sans", sans-serif'
});
text_box = $('<div></div>').appendTo(box_element).css({
"overflow": "hidden"
});
if (this.show_label) {
text_box.text(entry.label);
}
annotator = this;
box_element.hover((function(e) {
return close_button.show();
}), (function(e) {
return close_button.hide();
}));
close_button.mousedown(function(e) {
return annotator.hit_menuitem = true;
});
close_button.click(function(e) {
var clicked_box, index;
clicked_box = close_button.parent(".annotated_bounding_box");
index = clicked_box.prevAll(".annotated_bounding_box").length;
clicked_box.detach();
annotator.entries.splice(index, 1);
return annotator.onchange(annotator.entries);
});
return close_button.hide();
}
// Clear all entries.
clear_all(e) {
this.annotator_element.find(".annotated_bounding_box").detach();
this.entries.splice(0);
return this.onchange(this.entries);
}
// Add crosshair guide.
initialize_guide(options) {
this.guide_h = $('<div class="guide_h"></div>').appendTo(this.image_frame).css({
"border": "1px dotted " + (options.color || '#000'),
"height": "0",
"width": "100%",
"position": "absolute",
"top": "0",
"left": "0"
});
return this.guide_v = $('<div class="guide_v"></div>').appendTo(this.image_frame).css({
"border": "1px dotted " + (options.color || '#000'),
"height": "100%",
"width": "0",
"position": "absolute",
"top": "0",
"left": "0"
});
}
};
}).call(this);