R/qr_scan.R

Defines functions qr_plot qr_scan

Documented in qr_plot qr_scan

#' Scan a file or image for QR codes.
#' 
#' \code{qr_scan} reads in an image file or \pkg{magick} image object and first
#' tries to read it using the C++ engine, which is fast and robust at locating
#' the alignment patterns. However, it is less robust at decoding. If no codes
#' can be read, the image and corners are passed to the JS engine, which is
#' slower but has a higher success rate on cropped images.
#'
#' This function calls \code{\link{qr_scan_cpp}} and possibly
#' \code{\link{qr_scan_js_from_corners}}. Each function has a
#' double-\code{while} loop that progressively pushes mid-brightness pixels to
#' pure black, and if that fails, progressively pushes mid-brightness pixels to
#' pure white. This algorithm was developed for identifying QR codes on white
#' printed sheets in outdoor images, in bright sun with or without shadows. To
#' speed up scanning, you can use arguments \code{lighten = F, darken = F} which
#' will skip any thresholding. If you use both \code{lighten = T, darken = T},
#' scanning may be quite slow until a decodable QR code is found. In those cases,
#' a progress bar will attempt to be shown, if you have the \pkg{progress}
#' package (\url{https://github.com/r-lib/progress}) available on your machine.
#' 
#' To BYO algorithm, you can use those two functions as templates. For example,
#' \code{\link{image_morphology}} with \code{(..., morphology = "Open", kernel =
#' "Square:n")} (varying \code{n} from 2 to 10) may repair corrupted QR blocks.
#' 
#' 
#' @param image A path to a \pkg{magick}-readable file, e.g. jpg or png, or a \pkg{magick} object.
#' @param flop  Logical. Should the image be mirrored L-R before reading?
#' @param lighten Logical. Should under-exposed areas of the image be lightened to increase contrast? Useful for images in shadow. Default \code{FALSE}.
#' @param darken Logical. Should over-exposed areas of the image be darkened to increase contrast? Useful for images in bright light. Default \code{TRUE}.
#' @param plot  Logical. Should the image with any detected codes be shown after reading? (Requires ggplot2)
#' @param force_js Logical. Should the JS library run even if the C++ library is able to read the code?
#' @param no_js Logical. Never use the JS library, even if no QR codes are decoded.
#' @param verbose Logical. Should warnings print for potentially slow operations?
#' @return A list of dataframes, \strong{values} and \strong{points}, each with a column \strong{id}. 
qr_scan <- function(
  image, 
  flop = FALSE, 
  lighten = FALSE, 
  darken = TRUE, 
  plot = FALSE, 
  force_js = FALSE, 
  no_js = FALSE, 
  verbose = interactive()
  ) {
  if (is.character(image)) {
    mgk <- magick::image_read(image)
  } else if ("magick-image" %in% class(image)) {
    mgk <- image
  } else {
    stop("Supply either an image file path or a magick image object.")
  }
  
  if (flop) {
    mgk <- magick::image_flop(mgk)
  }
  
  code_obj <- qr_scan_cpp(mgk, flop = FALSE, lighten = lighten, darken = darken, verbose = verbose)
  code_pts <- code_obj$points
  
  # TODO image_destroy(mgk)
  
  if (isTRUE(nrow(code_pts) > 0) && any(code_obj$values$value == "") || force_js) {
    if (!no_js && qr_v8_checker_()) {
      code_list <- split(code_pts, code_pts$id)
      code_obj <- 
        purrr::map(
          code_list, 
          ~qr_scan_js_from_corners(mgk, .x, lighten = lighten, darken = darken, verbose = F)
          ) %>% 
        qr_parse_js_()
    }
  } 
  
  if (plot && requireNamespace("ggplot2", quietly = TRUE)) {
    print(qr_plot(mgk, code_obj))
  }
  
  cat("\n")
  return(code_obj)
}


#' Plot a \pkg{magick} image with any detected QR codes.
#' 
#' \code{qr_plot} is a helper called inside \code{qr_scan} to visualize images
#' and any QR codes. Requires \code{ggplot2}.
#' 
#' @param mgk       The image object that was scanned.
#' @param code_obj  A list of dataframes, \strong{values} and \strong{points}, generated by \code{qr_scan}.
#' @return A ggplot object.
qr_plot <- function(mgk, code_obj) {
  if (!requireNamespace("ggplot2", quietly = TRUE)) {
    stop("Plotting decoded QR images requires ggplot2.")
  }

  dat <- merge(code_obj$values, code_obj$points, all = T)
  
  # slow to resize large images, but slow to render them as well, how to fix?
  if (magick::image_info(mgk)$width > 1000) {
    mgk <- magick::image_resize(mgk, "50%")
    dat$x <- dat$x/2
    dat$y <- dat$y/2
  }  
  
  if (nrow(dat) == 0) {
    magick::image_ggplot(mgk)
  } else {
    centers <- stats::aggregate(
      cbind(x, y) ~ value+id,
      data = dat,
      FUN = mean
    )
    
    magick::image_ggplot(mgk) +
      ggplot2::geom_point(
        data = dat,
        ggplot2::aes(x, y, color = as.character(id)),
        show.legend = FALSE, size = 4
      ) +
      ggplot2::geom_label(
        data = centers,
        ggplot2::aes(x, y, label = value),
        fontface = "bold"
      )
  }
  
}
brianwdavis/quadrangle documentation built on Feb. 27, 2023, 6:36 p.m.