5

I have written a shiny app that allows the user to draw rectangles on top of an image (minimal reproducible example below).

The problem with my current approach is that every time a rectangle is added, a new image is created, written to disk, and rendered (sent to the user's browser). This takes quite some time, and becomes really annoying when the Internet connection is slow.

Is there any way to display the rectangles on top of the image directly in the browser, without modifying the image on the server side? The only thing I need to ensure is that the browser sends back to the server the rectangles coordinates over the plot.

A good example of what I'm looking for (in JavaScript): https://kyamagu.github.io/bbox-annotator/demo.html I know JavaScript can be embedded in a Shiny app through a widget, if no one proposes an easier solution, that's what I'll do.

library(shiny)
library(png)
library(RCurl)

myurl = 'https://raw.githubusercontent.com/Tixierae/deep_learning_NLP/master/CNN_IMDB/cnn_illustration.png'
my_img = readPNG(getURLContent(myurl))
img_height = dim(my_img)[1]
img_width = dim(my_img)[2]

server = function(input, output) {

    observe({

        outfile = tempfile(tmpdir='./', fileext='.png')

        png(filename=outfile,width=img_width,height=img_height)

        par(mar=c(0,0,0,0),xaxs='i', yaxs='i')
        plot(NA,xlim=c(0,img_width),ylim=c(0,img_height))
        rasterImage(my_img,0,0,img_width,img_height)

        if (!is.null(input$image_brush)){
            b_in = lapply(input$image_brush,as.numeric)
            if (!is.null(b_in$xmin)){
                rect(b_in$xmin,img_height-b_in$ymax,b_in$xmax,img_height-b_in$ymin,border='green',lwd=5)
            }
        }

        dev.off()

        output$my_image = renderImage({
            list(
                src = outfile,
                contentType = 'image/png',
                width = img_width,
                height = img_height,
                alt = ''
            )
        },deleteFile=TRUE)

        output$image = renderUI({
            imageOutput('my_image',
                height = img_height,
                width = img_width,
                click = 'image_click',
                dblclick = dblclickOpts(
                    id = 'image_dblclick'
                ),
                hover = hoverOpts(
                    id = 'image_hover'
                ),
                brush = brushOpts(
                    id = 'image_brush',resetOnNew=TRUE,delayType='debounce',delay=100000
                )
            )
        })
    })
}

ui = bootstrapPage(
    uiOutput('image')
)

shinyApp(ui=ui, server=server)
Antoine
  • 1,649
  • 4
  • 23
  • 50

1 Answers1

4

Here's a JS option based entirely on this answer.

enter image description here

# JS and CSS modified from: https://stackoverflow.com/a/17409472/8099834
css <- "
    #canvas {
        width:2000px;
        height:2000px;
        border: 10px solid transparent;
    }
    .rectangle {
        border: 5px solid #FFFF00;
        position: absolute;
    }
"

js <- 
"function initDraw(canvas) {
    var mouse = {
        x: 0,
        y: 0,
        startX: 0,
        startY: 0
    };
    function setMousePosition(e) {
        var ev = e || window.event; //Moz || IE
        if (ev.pageX) { //Moz
            mouse.x = ev.pageX + window.pageXOffset;
            mouse.y = ev.pageY + window.pageYOffset;
        } else if (ev.clientX) { //IE
            mouse.x = ev.clientX + document.body.scrollLeft;
            mouse.y = ev.clientY + document.body.scrollTop;
        }
    };

    var element = null;    
    canvas.onmousemove = function (e) {
        setMousePosition(e);
        if (element !== null) {
            element.style.width = Math.abs(mouse.x - mouse.startX) + 'px';
            element.style.height = Math.abs(mouse.y - mouse.startY) + 'px';
            element.style.left = (mouse.x - mouse.startX < 0) ? mouse.x + 'px' : mouse.startX + 'px';
            element.style.top = (mouse.y - mouse.startY < 0) ? mouse.y + 'px' : mouse.startY + 'px';
        }
    }

    canvas.onclick = function (e) {
        if (element !== null) {
           var coord = {
               left: element.style.left,
               top: element.style.top,
               width: element.style.width,
               height: element.style.height
            };
            Shiny.onInputChange('rectCoord', coord);
            element = null;
            canvas.style.cursor = \"default\";
        } else {
            mouse.startX = mouse.x;
            mouse.startY = mouse.y;
            element = document.createElement('div');
            element.className = 'rectangle'
            element.style.left = mouse.x + 'px';
            element.style.top = mouse.y + 'px';
            canvas.appendChild(element);
            canvas.style.cursor = \"crosshair\";
        }
    }
};
$(document).on('shiny:sessioninitialized', function(event) {
    initDraw(document.getElementById('canvas'));
});
"

library(shiny)

ui <- fluidPage(
  tags$head(
      tags$style(css),
      tags$script(HTML(js))
  ),
  fluidRow(
      column(width = 6, 
             # inline is necessary
             # ...otherwise we can draw rectangles over entire fluidRow
             uiOutput("canvas", inline = TRUE)),
      column(
          width = 6,
          verbatimTextOutput("rectCoordOutput")
          )
  )
)

server <- function(input, output, session) {
    output$canvas <- renderUI({
        tags$img(src = "https://www.r-project.org/logo/Rlogo.png")
    })
    output$rectCoordOutput <- renderPrint({
        input$rectCoord
    })

}

shinyApp(ui, server)
Hallie Swan
  • 2,714
  • 1
  • 15
  • 23
  • amazing, thank you so much! I will very likely accept your answer and award your bounty to you, I'm just waiting a few hours more just in case someone else wants to contribute. Would you know by any chance how to remove a rectangle by double clicking inside it? (the inner most rectangle in case of multiple matches). This was not part of the original question, so I'll understand if you don't want to answer that. – Antoine Jan 22 '20 at 10:27
  • actually, I've asked this follow-up question as a new thread: https://stackoverflow.com/questions/59860206/drawing-rectangles-on-top-of-image-r-shiny so that in case you want to elaborate on your answer, you can be rewarded for that – Antoine Jan 22 '20 at 12:53
  • glad it's helpful :) I don't know how to remove the rectangle, but I'll look into it! – Hallie Swan Jan 22 '20 at 17:11
  • Thanks, I hope you can contribute something! – Antoine Jan 22 '20 at 18:03
  • just to let you know I've started a bounty on the new question, in case you want to jump in – Antoine Jan 29 '20 at 08:38
  • @HallieSwan, I am trying to draw a circle on visnetwork canvas id in Shiny. Can you please have a look and appreciate if you can provide how to approach this. [Link to the SO Question](https://stackoverflow.com/questions/65583493/draw-circle-on-vis-network-canvas-and-arrange-nodes?noredirect=1#comment116495268_65583493) – user5249203 Jan 25 '21 at 17:52
  • I have tested this answer and I found when the image is too large to fit in the windows and I have scroll down, it draws the rectangle in the wrong place... :( – KH Kim Sep 21 '21 at 12:45
  • @KHKim sorry to hear that -- I haven't tested your use case, but [this solution](https://stackoverflow.com/questions/59860206/drawing-rectangles-on-top-of-image-r-shiny) might be more robust – Hallie Swan Sep 21 '21 at 13:27
  • @HallieSwan Yes, it really is. Thanks for directing me to the answer. – KH Kim Sep 21 '21 at 13:39