Skip to content
Snippets Groups Projects
utils.R 3 KiB
Newer Older
Gilquin's avatar
Gilquin committed
####
# Additional utility functions.
####

# global imports
import::here("cluster", "ellipsoidhull", .character_only = TRUE)
import::here(
  "imager",
  c(
    "as.pixset", "contours", "is.cimg", "is.pixset", "%inr%",
    "mopening_square", "where"
  ),
  .character_only = TRUE
)
import::here("mclust", "densityMclust", .character_only = TRUE)
import::here("purrr", c("keep", "map"), .character_only = TRUE)


#' Sample pixel values from histogram
#'
#' @param obj an array-like object (img, pixset)
#' @param ratio a real, the sampling ratio.
#'
#' @returns an integer vector
sample_histogram <- function(obj, ratio = 0.02) {
  n_samples <- min(1e4L, prod(dim(obj)[1L:2L]))
  n_breaks <- as.integer(n_samples * ratio)
  hist <- hist(obj, breaks = n_breaks, plot = FALSE)
  bins <- with(
    hist, sample(length(mids), n_samples, p = density, replace = TRUE)
  )
  sample <- runif(
    length(bins), hist$breaks[bins], hist$breaks[bins + 1L]
  )
  return(sample)
}


#' Square stencil
#'
#' Define a square stencil within the boundaries of an image cropping points
#' that are out of bounds.
#'
#' @param centroid an integer vector, the stencil centroid.
#' @param pad an integer, the padding around the stencil centroid.
#' @param obj an imager::cimg or an imager::pixset
#'
#' @returns a data.frame
square_stencil <- function(centroid, pad, obj) {

  # get image dimensions
  if (is.cimg(obj) | is.pixset(obj)) {
    dims <- as.integer(dim(obj)[1L:2L])
  } else {
    stop("Invalid 'obj' argument.")
  }
  # check bounds
  bounds <- c(
    ifelse(centroid - pad < 1L, -centroid + 1L, -pad),
    ifelse(pad + centroid > dims, dims - centroid, pad)
  )
  # define stencil
  stencil <- expand.grid(
    dx = seq(bounds[1L], bounds[3L]),
    dy = seq(bounds[2L], bounds[4L])
  )
  return(stencil)
}


#' Centimeter to pixel
#'
#' Get the centimeter to pixel conversion factor from a ruler defined as
#' a pixset.
#'
#' @param ruler an imager::pixset
#' @param quantile a real, the quantile level used for thresholding.
#'
#' @returns a data.frame
cm_to_pixel <- function(ruler, quantile = 5e-3) {

  # get ruler contours size
  ct_size <- ruler %>%
    contours(nlevels = 1L) %>%
    map(~ length(.$x)) %>%
    unlist()
  # estimate the distribution by a Gaussian
  gm <- densityMclust(log10(ct_size), G = 1L, plot = FALSE)
  # threshold to discriminate square contours
  thresholds <- qnorm(
    c(quantile, 1L - quantile),
    gm$parameters$mean[1L],
    sqrt(gm$parameters$variance$sigmasq[1L])
  )
  # get the conversion factor cm to pixel
  conv_factor <- ct_size %>%
    keep(~ (. %inr% 10L ** thresholds)) %>%
    {. / 4L} %>%
    mean()
  return(conv_factor)
}


#' Ellipsoid hull
#'
#' Fit an ellipsoid hull for a pixset with optional morphological opening.
#'
#' @param pixset an imager::pixset
#' @param size an integer, the morphological opening factor.
#'
#' @returns a data.frame
ehull <- function(pixset, size = 6L) {
  out <- pixset %>%
    mopening_square(size) %>%
    as.pixset() %>%
    where() %>%
    as.matrix() %>%
    ellipsoidhull()
  return(out)
}