Draw an uploaded image file in R shiny app using the magick package

Hi,

I would like to create a Shiny app that allows user to upload a csv and image files and draw the image file by placing rectangles according to the pixel coordinate data in the uploaded csv file. The file uploading part is working. I am trying to get the image drawing part working with Shiny using the following functions from the magick package:

  • image <- magick::image_read(req(input$imported$datapath)), which reads the uploaded image (inputID="imported") as a magick-image object. This is working.
  • magick::image_draw(image=image()), which allows me to draw the imported image in a local R script But here it has returned an error "Warning: Error in image.default: no 'z' matrix specified."
  • graphics::rect(), which places rectangles on the image according to the four position arguments xleft, xright, ytop, and ybottom. This is working in my local R script. I have commented this part out in the provided server.R file.
  • grDevices::dev.off(), which closes the image device in my local R script. This part is commented out in the server.R

I am looking to do the file uploading and image annotation under different tabPanels listed at the end of the server.R. Any thoughts on making the image_draw() function working with Shiny? Here is a printscreen of the tabPanel that returns the error

My ui.R

library(shiny)
library(magick)

# Define User Interface
shinyUI(fluidPage(
  navbarPage( 
    # App title ----
    titlePanel("File Upload"),
    # Sidebar layout with input and output definitions ----
    sidebarLayout(
      sidebarPanel(
        # fileinput() function is used to get the file upload control option
        fileInput(inputId = "file", label = "Upload a text or csv file"),
        helpText("Default max. file size is 5MB"),
        # Add a horizontal line
        tags$hr(),
        # Add a level 4 header
        h4(helpText("Select the read.table parameters below")),
        checkboxInput(inputId = 'header', label = 'Header', value = FALSE),
        checkboxInput(inputId = "stringAsFactors", "stringAsFactors", FALSE),
        br(),
        radioButtons( inputId = 'sep'
                      ,label = 'Separator'
                      ,choices = c(Comma=',',Semicolon=';',Tab='\t', Space='')
                      ,selected = ','),
        # Add a horizontal line
        tags$hr(),
        h4(helpText("Select number of bins for histogram")),
        # Input: Slider for the number of bins ----
        sliderInput(inputId = "bins",
                    label = "Number of bins:",
                    min = 1,
                    max = 50,
                    value = 30),
        # Add a horizontal line
        tags$hr(),
        fileInput(inputId = "imported", label = "Upload an image file"),
        helpText("Accepting file formats: jpg, png")
        ),
      mainPanel(
        uiOutput("tb") 
      )
    )
  ))
)

My server.R

shinyServer(function(input,output){
  
  # This reactive function will take the inputs from ui.R and use them for read.table() to read the data from the file. It returns the dataset in the form of a data.frame.
  # To use the input, use input$inputID (e.g., input$file)
  # To use the data data.frame, refer it as "data()" rather than "data"
  data <- reactive({
    file1 <- input$file
    if(is.null(file1)){return()} 
    # file$datapath -> gives the path of the file
    read.table( file=file1$datapath
               ,sep=input$sep
               ,header = input$header
               ,stringsAsFactors = input$stringAsFactors)
  })
  
  # This reactive output contains the summary of the dataset and display the summary in table format
  output$file_1 <- renderTable({
    if(is.null(data())){return ()}
    input$file
  })
  
  # This reactive output contains the summary of the dataset and display the summary in table format
  output$file_2 <- renderTable(
    input$imported
  )
  
  # This reactive output contains the summary of the dataset and display the summary in table format
  output$sum <- renderTable({
    if(is.null(data())){return ()}
    summary(data())
  })
  
  # This reactive output contains the dataset and display the dataset in table format
  output$table <- renderTable({
    if(is.null(data())){return ()}
    data()
  })
  
  # Make a histogram on column 3 of input data file
  output$distPlot <- renderPlot({
    # Get column 3
    x <- data()[,3]
    x <- na.omit(x)
    bins <- seq(min(x), max(x), length.out = input$bins + 1)
    
    hist(x
         ,breaks = bins
         , col = "#75AADB"
         , border = "black"
         ,xlab = "Marker measurement"
         ,main = "Histogram of marker measurement"
         ,density=TRUE )
  })
  
  # Display the uploaded image
  output$image <- renderImage({
    # The req function makes sure a input was actually selected
    req(input$imported)
    list(
       src    = normalizePath(file.path(input$imported$datapath))
      ,alt    = "there should be an image here"
      #,width  = 400
      #,height = 400
      )
    }, deleteFile = FALSE)
  
  #--------------------------------------------------
  # Edit the uploaded image file using magick package
  #--------------------------------------------------
  loaded_image <- reactive({
    # Create pixel coordinates for rectangles 
    rect_width_half <- 12.5
    rect_x_left <- data()[,1] - rect_width_half
    rect_x_right <- data()[,1] + rect_width_half 
    rect_y_top <- data()[,2] - rect_width_half 
    rect_y_bottom <- data()[,2] + rect_width_half 
    
    image <- magick::image_read(req(input$imported$datapath))
    magick::image_draw(image=image())
    #return(image_drew)
    # image <- magick::image_read(req(input$imported$datapath))
    # magick::image_draw(image())
    # ## Draw rectangles for individual stained cells
    # graphics::rect( xleft= rect_x_left
    #                 ,ybottom=rect_y_bottom
    #                 ,xright=rect_x_right
    #                 ,ytop=rect_y_top
    #                 ,col = NA # color(s) to fill or shade the rectangle(s) with. The default NA (or also NULL) means do not fill, i.e., draw transparent rectangles, unless density is specified.
    #                 ,border = "black" # color for rectangle border(s).
    #                 ,lty = par("lty")
    #                 ,lwd = 2)
    #grDevices::dev.off()
    
  })
  
  output$image_edited <- renderPlot({
    image_ggplot(loaded_image())
  })

  # The following renderUI is used to dynamically generate the tabsets when the file is loaded. Until the file is loaded, app will not show the tabset.
  output$tb <- renderUI({
    if(is.null(data()))
      h5("Powered by", tags$img(src='RStudio-Ball.png', heigth=200, width=200))
    else
      tabsetPanel( tabPanel("About file", tableOutput(outputId ="file_1"))
                  ,tabPanel("About image", tableOutput(outputId = "file_2")) 
                  ,tabPanel("Data", tableOutput(outputId ="table"))
                  ,tabPanel("Summary", tableOutput(outputId ="sum"))
                  ,tabPanel("Histogram",plotOutput(outputId = "distPlot"))
                  ,tabPanel("Image", imageOutput(outputId = "image"))
                  ,tabPanel("Annotate", imageOutput(outputId = "image_edited"))
                  )
  })
})

Any feedback would be highly appreciated.
Chang

I question the use of brackets after image on the 2nd line.

Hi nirrahamuk,
Thanks for the comment. I've removed the (). A different error pops up in my R console when I rerun the app:

Listening on http://127.0.0.1:3637
Warning: Error in grid::grid.newpage: Cannot open a new page on a drawing device
174:
The error shown under my tabPanel:

My code for the reactive object:

  loaded_image <- reactive({
    # Create pixel coordinates for rectangles 
    rect_width_half <- 12.5
    rect_x_left <- data()[,1] - rect_width_half
    rect_x_right <- data()[,1] + rect_width_half 
    rect_y_top <- data()[,2] - rect_width_half 
    rect_y_bottom <- data()[,2] + rect_width_half 
    
    image <- magick::image_read(req(input$imported$datapath))
    magick::image_draw(image=image)
    #return(image_drew)
    # image <- magick::image_read(req(input$imported$datapath))
    # magick::image_draw(image())
    # ## Draw rectangles for individual stained cells
    # graphics::rect( xleft= rect_x_left
    #                 ,ybottom=rect_y_bottom
    #                 ,xright=rect_x_right
    #                 ,ytop=rect_y_top
    #                 ,col = NA # color(s) to fill or shade the rectangle(s) with. The default NA (or also NULL) means do not fill, i.e., draw transparent rectangles, unless density is specified.
    #                 ,border = "black" # color for rectangle border(s).
    #                 ,lty = par("lty")
    #                 ,lwd = 2)
    #grDevices::dev.off()
    
  })

I would start small and build up.
Does this work for you as a basis ?

library(shiny)
library(magick)
library(tidyverse)
ui <- fluidPage(
  shiny::checkboxInput("image_use_switch",
                       label="show an image?"),
  plotOutput("example_image")
)

server <- function(input, output, session) {
 
  loaded_image <- reactive({
    if(isTruthy(input$image_use_switch))
       return( magick::logo )
    else
      return (NULL)
  })
  output$example_image <- renderPlot({
    image_ggplot(req(loaded_image()))
  })
   
}

shinyApp(ui, server)

Hi,
Yes, the provided code is working. Here is a printscreen

Great, for your next step I would suggest firstly having your own image from file loaded by magick in place of the wizard / magick::logo

once you crack that....
Try to make it happen from a fileUpload