Drawing rectangles on top of image R shiny

后端 未结 1 1576
爱一瞬间的悲伤
爱一瞬间的悲伤 2021-02-08 01:35

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

1条回答
  •  予麋鹿
    予麋鹿 (楼主)
    2021-02-08 02:16

    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:

    1. Choosing a different image url will remove previous rectangles
    2. I edited the JS a bit to change the rectangle/text color, so you won't be able to pull directly from the original repo
    3. My changes probably broke input_method = "fixed" and "text", I only tested input_method = "select"

    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(
                    ''
                )),
                HTML(
                    ''
                ),
                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 = $('
    '); 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 = $('
    '); 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 = $(''); this.label_box.append(this.label_input); this.label_input.append($('')); ref = options.labels; for (i = 0, len = ref.length; i < len; i++) { label = ref[i]; this.label_input.append(''); } this.label_input.change(function(e) { return this.blur(); }); break; case 'text': if (typeof options.labels === "string") { options.labels = [options.labels]; } this.label_input = $(''); 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 = $(''); 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 = $('
    '); 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 = $('
    '); 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 = $('
    ').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" }); $("
    ").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 = $('
    ').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 = $('
    ').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 = $('
    ').appendTo(this.image_frame).css({ "border": "1px dotted " + (options.color || '#000'), "height": "100%", "width": "0", "position": "absolute", "top": "0", "left": "0" }); } }; }).call(this);

    0 讨论(0)
提交回复
热议问题