R/pictplot.R

Defines functions create_dead_leaves Reinhard_multiple Reinhard_single color_transfer lab2LMS LMS2lab LMS2RGB RGB2LMS visualize_contrast CIELAB_visualize color_dodge PHOG_features PHOG_descriptor get_hog get_gradient_lab get_gradient FD_nr DBC Fractal2D edge_density im_symmetry palette_visualize palette_extract circular_color_stat angle_distribution im_distribute im_moments get_moments im_diff fft_transfer fft_noise fft_slope fft_angular_mask fft_spectrum fft_phase fft_amplitude fft_phase_scramble fft_filter fft_lowpass_kernel inv_fft for_fft fft_shift im_blur filter_v1 v1_kernels V1_params im_conv stat_filter guided_filter gabor_kernel gauss_kernel box_variance box_blur edge_XDOG edge_DOG edge_canny edge_maxmin edge_variance edge_prewitt edge_sobel edge_diff paint_arc paint_circle get_oval_mask get_circular_mask get_arc_mask get_L im_gray im_serialize_inv im_serialize apply_oval_mask im_glitch im_randomize im_random_pixelate im_pixelate im_raise im_threshold im_combine im_resize_scale im_resize_limit_min im_resize_limit im_resize im_flip_position im_flip im_rotate im_crop_square im_get im_crop im_shift im_pad im_tricolored im_rep im_reverse fade_mask fade_border im_blend remove_alpha remove_channel get_alpha im_pile merge_color split_color get_A get_B get_G get_R get_channel force_channel_label_to_num im_coord im_cy im_cx im_nc im_nchannel im_npix im_diagonal im_size im_width im_height im_noise im_mono is_inside_ellipse find_circle smart.round logspace ramp_threshold MaxMin pow vec_shift eucdist cubic_spline clamping rescaling rescaling01 YUV2sRGB sRGB2YUV YUV2RGB RGB2YUV Lab2RGB RGB2Lab Lab2sRGB sRGB2Lab Lab2XYZ XYZ2Lab XYZ2sRGB sRGB2XYZ XYZ2RGB RGB2XYZ sRGB2HSL RGB2sRGB sRGB2RGB colorder list2matrix matrix2list resetPar as.data.frame.nimg nimg2cimg cimg2nimg im_save get_image_name_from_file im_load_dir im_load pplot plot.nimg print.nimg is.nimg nimg

Documented in angle_distribution apply_oval_mask as.data.frame.nimg box_blur box_variance CIELAB_visualize cimg2nimg circular_color_stat clamping colorder color_dodge create_dead_leaves cubic_spline DBC edge_canny edge_density edge_diff edge_maxmin edge_prewitt edge_sobel edge_variance eucdist fade_border fade_mask fft_amplitude fft_angular_mask fft_filter fft_lowpass_kernel fft_noise fft_phase fft_phase_scramble fft_shift fft_slope fft_spectrum filter_v1 find_circle for_fft Fractal2D gabor_kernel gauss_kernel get_A get_alpha get_arc_mask get_B get_channel get_circular_mask get_G get_gradient get_hog get_L get_moments get_oval_mask get_R guided_filter im_blend im_blur im_combine im_conv im_coord im_crop im_crop_square im_cx im_cy im_diagonal im_diff im_distribute im_flip im_flip_position im_get im_gray im_height im_load im_load_dir im_moments im_mono im_nc im_nchannel im_noise im_npix im_pad im_pile im_pixelate im_raise im_randomize im_random_pixelate im_rep im_resize im_resize_limit im_resize_limit_min im_resize_scale im_reverse im_rotate im_save im_serialize im_serialize_inv im_shift im_size im_symmetry im_threshold im_tricolored im_width inv_fft is_inside_ellipse is.nimg Lab2RGB Lab2sRGB Lab2XYZ logspace merge_color nimg nimg2cimg paint_arc paint_circle palette_extract palette_visualize PHOG_descriptor PHOG_features plot.nimg pow pplot ramp_threshold remove_alpha remove_channel rescaling rescaling01 RGB2Lab RGB2sRGB RGB2XYZ RGB2YUV smart.round split_color sRGB2HSL sRGB2Lab sRGB2RGB sRGB2XYZ sRGB2YUV stat_filter v1_kernels V1_params vec_shift XYZ2Lab XYZ2RGB XYZ2sRGB YUV2RGB YUV2sRGB

#' @importFrom stats runif median
#' @importFrom stringr str_match str_split str_sub
#' @importFrom graphics plot
#' @importFrom magrittr mod "%>%"
#' @import ggplot2
NULL


# CRAN sometimes issues spurious warnings about undefined variables
utils::globalVariables( c( ".", "%>%", "x", "y", "c", "value" ) )



# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# nimg class ----


#' nimg is a class for storing image data as a 3 dimensional array (y, x, c format).
#' y and x are spatial coordinates, and c is a color dimension.
#' @title Create a nimg object
#' @param im a 3 dimensional numeric array
#' @param name a string for name attribute.
#' @return an object of class nimg
#' @examples
#' nimg(array(1,c(128,256,3)))
#' @export
nimg = function( im, name ){
  if( is.logical( im ) | is.integer( im ) ){
    im = im + 0.0
  }
  if( length( dim( im ) ) == 2 ){ # gray-scale image
    dim( im ) = c( dim( im ), 1 )
  }
  class( im ) = c( "nimg", "numeric" )
  if( ! base::missing( name ) ){
    attr( im, "name" ) = name
  } else if( is.null( attr( im, "name" ) ) ){
    attr( im, "name" ) = ""
  }
  im
}


##' Check if an object is a nimg object
##' @param x an object
##' @return logical. if an object is a nimg object.
##' @examples
##' is.nimg(regatta)
##' @export
is.nimg = function( x ){
  methods::is(x,"nimg")
}


##' @export
print.nimg = function( x, ... ){
  d = dim( x )
  if( attr( x, "name" ) == "" || attr( x, "name" ) == "-" || is.null( attr( x, "name" ) ) ){
    name = "image"
  } else {
    name = attr( x, "name" )
  }
  cat( sprintf( "%s: %i [height] x %i [width] x %i [colour channels]\n", name, d[1], d[2], d[3] ) )
  # cat( sprintf( "image: %i [height] x %i [width] x %i [colour channels]\n", d[1], d[2], d[3] ) )
  invisible( x )
}


#' Display an image
#' @param x an image
#' @param rescale logical. if true, then pixel value is rescaled to range between 0 and 1.
#' @param ... other parameters to be passed to plot.default
#' @return No return value, called for side effects.
#' @examples
#' plot(regatta)
#' @export
plot.nimg = function( x, rescale = FALSE, ... ){
  old.par = graphics::par( no.readonly = TRUE )
  on.exit( graphics::par( old.par ), add = TRUE )
  if( im_npix( x ) == 0 ){
    stop( "The image is empty." )
  }

  if( im_nchannel( x ) == 1 ){
    # a raster array must have exactly 3 or 4 planes
    x = im_rep( x, 3 )
  }
  im = x[ ,,, drop = FALSE ]
  if( rescale ){
    im = rescaling01( im )
  } else if( max( im ) > 1 || min( im ) < 0 ){
    warning( paste0( "Pixcel value exceeds the range [0,1], and hence it was clamped when plotting.\n",
                     "min = ", min( im ), ", max = ", max( im ) ) )
    im = clamping( im )
  }
  graphics::par( mar = c( 0, 0, 0, 0 ) )
  graphics::plot.new()
  graphics::plot.window(
    xlim = c(1,im_width(x)), ylim = c(im_height(x),1), asp = 1, xaxs = "i", yaxs = "i", ...)
  rst = grDevices::as.raster(
    im, rescale = FALSE, colorscale = NULL, colourscale = NULL, col.na = grDevices::rgb(0,0,0,0) )
  graphics::rasterImage( rst, 1, nrow( rst ), ncol( rst ), 1, interpolate = FALSE )
  invisible( x )
}


#' Display an image
#' @param im an image. either nimg object or an array object
#' @param rescale logical. if true, then pixel value is rescaled to range between 0 and 1.
#' @return No return value, called for side effects.
#' @examples
#' pplot(regatta)
#' @export
pplot = function( im, rescale = FALSE ){
  if( any( c( "nimg", "cimg", "pixset" ) %in% class( im ) ) ){
    graphics::plot( im, rescale = rescale )
  } else {
    graphics::plot( nimg( im ), rescale = rescale )
  }
}


#' Load image from file or URL
#' @param file path to file or URL
#' @param name a string for name attribute. if missing, inferred from the file argument.
#' @return an array of image data
#' @examples
#' \dontrun{
#' # load an image from disk
#' im = im_load("path/to/your/image.jpg")
#' plot(im)
#' # load an image from URL
#' im = im_load("http://placehold.jp/150x150.png")
#' }
#' @export
im_load = function( file, name ){
  if( grepl("^(http|ftp)s?://", file) ){ # if URL
    url = file
    ext = stringr::str_extract_all( url, "\\.([A-Za-z0-9]+$)" )[[ 1 ]]
    if( length( ext ) > 0 ){
      file = tempfile( fileext = ext )
    } else {
      file = tempfile()
    }
    downloader::download( url, file, mode = "wb" )
    im = im_load( file, get_image_name_from_file( url ) )
    unlink( file )
    return( im )
  }
  ext = sub( ".*\\.([^.]{3,4})$", "\\1", file ) %>% tolower
  if( ext %in% c( "png", "bmp", "jpg", "jpeg" ) ){
    tryCatch({
      im = readbitmap::read.bitmap( file )
    },
    error = function(e) {
      stop( paste0( e, "Note: im_load() fails for binary (black/white) bmp image." ) )
    })
    # im = readbitmap::read.bitmap( file )
    dim( im )
    if( ! is.null( attr( im, "header" ) ) ){
      im = im / 255
    }
    if( length( dim( im ) ) == 2 ){ # gray-scale image
      dim( im ) = c( dim( im ), 1 )
    } else if( length( dim( im ) ) == 3 ){ # multiple channels
      if( dim( im )[ 3 ] %in% c( 2, 4 ) ){
        # remove alpha channel if it is uninformative
        if( min( im[ , , dim( im )[ 3 ] ] ) == max( im[ , , dim( im )[ 3 ] ] ) ){
          im = im[ , , 1:( dim( im )[ 3 ] - 1 ), drop = FALSE ]
        }
      }
    }
    im = nimg( im, ifelse( base::missing( name ), get_image_name_from_file( file ), name ) )
    return( im )
  } else {
    stop( "Only jpg, png, and bmp formats are supported." )
  }
}


#' Load all images in a directory and return them as a list
#' @param path a directory path containing images
#' @return a list of images
#' @examples
#' \dontrun{
#' im = im_load_dir( "path/to/image/folder" )
#' }
#' @export
im_load_dir = function( path ){
  names = list.files( path, pattern = "\\.(jpg|jpeg|png|bmp|JPG|JPEG|PNG|BMP)$" )
  l = vector( "list", length( names ) )
  for( i in 1:length( names ) ){
    tryCatch({
      l[[ i ]] = im_load( paste0( path, "/", names[ i ] ) )
    },
    error = function(e) {
      warning( paste0( names[ i ], " cannot be loaded, and ignored." ) )
      l[[ i ]] = NULL
    })
    names( l )[ i ] = stringr::str_split( names[ i ], "[.]" )[[ 1 ]][ 1 ]
  }
  # names( l ) = names
  return( l )
}


get_image_name_from_file = function( file ){
  tryCatch({
    name = stringr::str_split( file, "/" )[[ 1 ]]
    name = name[ length( name ) ]
    name = stringr::str_split( name, "[.]" )[[ 1 ]]
    return( name[ 1 ] )
  },
  error = function(e) {
    return( "-" )
  })
}


#' Save an image to disk
#' @param im An image.
#' @param name Name of the image file.
#' @param path Path to file.
#' @param format Image format. Either "jpg", "png", "tiff", or "bmp". Default is "png".
#' @param quality (jpg only) default is 0.95. Higher quality means less compression.
#' @return No return value, called for side effects.
#' @examples
#' \dontrun{
#' # regatta.png is saved to the current working directory
#' imsave( regatta, path = getwd() )
#' # myimage.jpg is saved to a specified directory
#' imsave( regatta, name = "myimage", path = "path/to/image", format = "jpg" )
#' }
#' @export
im_save = function( im, name, path, format = "png", quality = .95 ){
  if( ! format %in% c( "jpg", "png" ) ){
    warning( "Incorrect imaeg format. Use either jpg or png." )
    return()
  }
  if( base::missing( name ) ){
    name = deparse( substitute( im ) )
  }
  if( im_nchannel( im ) == 1 ){
    im = im_rep( im, 3 )
  }
  if( stringr::str_sub( path, stringr::str_length( path ) ) == "/" ){
    path = stringr::str_sub( path, end = stringr::str_length( path ) - 1 )
  }
  if( max( im ) > 1 || min( im ) < 0 ){
    warning( "Pixcel value exceeds the range [0,1], and hence it was clamped when saving.")
    im = clamping( im )
  }
  base::dir.create( path, showWarnings = FALSE, recursive = TRUE )
  file = paste0( path, "/", name, ".", format )
  if( format == "png" ){
    png::writePNG( im, file )
  } else if ( format == "jpeg" | format == "jpg" ){
    jpeg::writeJPEG( im, file, quality = quality )
  }
}


#' cimg to nimg conversion
#' @param im a cimg object
#' @return an nimg object
#' @export
cimg2nimg = function( im ){
  if( is.list( im ) ){
    im = lapply( im, function( x ){
      if( "nimg" %in% class( x ) ){
        x
      } else {
        cimg2nimg( x )
      }
    })
    return( im )
  } else if( any( c( "cimg", "pixset" ) %in% class( im ) ) ){
    im = aperm( im, c( 2, 1, 4, 3 ) ) # (x, y, z, cc) to (y, x, cc, z)
    return( nimg( im[,,,1] ) )
  } else if( "nimg" %in% class( im ) ){
    return( im )
  } else {
    return( nimg( im ) )
  }
}


#' nimg to cimg conversion
#' @param im an nimg object
#' @return a cimg object
#' @export
nimg2cimg = function( im ){
  if( is.list( im ) ){
    im = lapply( im, function(x){
      if( any( c( "cimg", "pixset" ) %in% class( x ) ) ){
        x
      } else {
        nimg2cimg( x )
      }
    })
    return( im )
  } else {
    if( any( c( "cimg", "pixset" ) %in% class( im ) ) ) {
      return( im )
    } else if( length( dim( im ) ) == 2 ){ # (y, x) to (x, y)
      return( imager::as.cimg( t( im ) ) )
    } else if( length( dim( im ) ) == 4 ){ # (y, x, cc, z) to (x, y, z, cc)
      return( imager::as.cimg( aperm( im, c( 2, 1, 4, 3 ) ) ) )
    } else if( length( dim( im ) ) == 3 ){ # (y, x, cc) to (x, y, cc)
      im = aperm( im, c( 2, 1, 3 ) )
      im2 = array( 0, dim = c( dim( im )[ 1 ], dim( im )[ 2 ], 1, dim( im )[ 3 ] ) )
      im2[,,1,] = im
      return( imager::as.cimg( im2 ) )
    }
  }
}


#' nimg to data frame conversion
#' @param x an image
#' @param ... S3 generic/method consistency
#' @param is.wide either TRUE for wide format or FALSE for long format
#' @return a data frame
#' @export
as.data.frame.nimg = function( x, ..., is.wide = TRUE ){
  im = x
  ny = im_height( im )
  nx = im_width( im )
  nc = im_nc( im )
  if( length( dim( im ) ) == 2 ){ # (y, x) to (x, y)
    dat = t( im )
  } else if( length( dim( im ) ) == 4 ){ # (y, x, cc, z) to (x, y, z, cc)
    dat = aperm( im, c( 2, 1, 4, 3 ) )
  } else if( length( dim( im ) ) == 3 ){ # (y, x, cc) to (x, y, cc)
    dat = aperm( im, c( 2, 1, 3 ) )
  }
  df = data.frame(
    y = rep( 1:ny, each = nx, times = 1 * nc ),
    x = rep( 1:nx, each = 1, times = ny * nc ),
    color = rep( 1:nc, each = ny * nx, times = 1 ),
    value = as.numeric( dat )
  )
  if( is.wide ){
    df = tidyr::pivot_wider( df, names_from = "color", values_from = "value" )
    names( df ) = c( "y", "x", "R", "G", "B", "A" )[ 1:( 2 + nc ) ]
    if( nc == 1 || nc >= 3 ){
      if( nc == 1 ){
        df$G = df$R
        df$B = df$R
      }
      df$color = grDevices::rgb( df$R, df$G, df$B )
    }
  }
  return( df )
}


resetPar = function() {
  p = list(
    xlog = FALSE, ylog = FALSE, adj = 0.5, ann = TRUE, ask = FALSE, bg = "white", bty = "o",
    cex = 1, cex.axis = 1, cex.lab = 1, cex.main = 1.2, cex.sub = 1,
    col = "black", col.axis = "black", col.lab = "black", col.main = "black", col.sub = "black",
    crt = 0, err = 0, family = "", fg = "black", fig = c(0, 1, 0, 1), fin = c(6.239583, 5.6875),
    font = 1, font.axis = 1, font.lab = 1, font.main = 2, font.sub = 1,
    lab = c(5, 5, 7), las = 0, lend = "round", lheight = 1, ljoin = "round", lmitre = 10,
    lty = "solid", lwd = 1,
    mai = c(1.02, 0.82, 0.82, 0.42), mar = c(5.1, 4.1, 4.1, 2.1), mex = 1,
    mfcol = c(1, 1), mfg = rep(1, 4), mfrow = c(1, 1), mgp = c(3, 1, 0), mkh = 0.001,
    new = FALSE, oma = c(0, 0, 0, 0), omd = c(0, 1, 0, 1), omi = rep(0, 4),
    pch = 1, pin = c(4.999583, 3.8475), plt = c(0.131419, 0.9326878, 0.1793407, 0.8558242),
    ps = 12, pty = "m", smo = 1, srt = 0, tck = NA, tcl = -0.5, usr = c(0, 1, 0, 1),
    xaxp = c(0, 1, 5), xaxs = "r", xaxt = "s", xpd = FALSE,
    yaxp = c(0, 1, 5), yaxs = "r", yaxt = "s", ylbias = 0.2
  )
  graphics::par( p )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Data structure ----


matrix2list = function( x ){
  lapply(seq_len(ncol(x)), function(i) x[,i])
}


list2matrix = function( x ){
  matrix(unlist(x, use.names = FALSE), ncol = length( x ), byrow = FALSE)
}


#' change the column order of a data frame
#' @param df a data frame
#' @param name column names
#' @return a data frame
#' @export
colorder = function( df, name ){
  df[ , c( name, setdiff( names( df ), name ) ) ]
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Color space ----


#' sRGB to linear RGB conversion
#' @param im an image
#' @return an image
#' @export
sRGB2RGB = function( im ){
  mask = im < 0.04045
  im[ mask ] = im[ mask ] / 12.92
  im[ !mask ] = ( ( im[ !mask ] + 0.055 ) / 1.055 )^2.4
  return( im )
}


#' linear RGB to sRGB conversion
#' @param im an image
#' @return an image
#' @export
RGB2sRGB = function( im ){
  mask = im < 0.0031308
  im[ mask ] = im[ mask ] * 12.92
  im[ !mask ] = 1.055 * im[ !mask ]^( 1 / 2.4 ) - 0.055
  return( im )
}


#' sRGB to HSL conversion
#' @param im an image
#' @return an image
#' @export
sRGB2HSL = function( im ){
  cimg = nimg2cimg( im_tricolored( im ) )
  M = pmax( imager::R( cimg ), imager::G( cimg ), imager::B( cimg ) )
  m = pmin( imager::R( cimg ), imager::G( cimg ), imager::B( cimg ) )
  C = M - m
  # calculate H
  hue = imager::cimg( array( 0, c( dim( cimg )[ 1:3 ], 1 ) ) )
  H1 = ( imager::G( cimg ) - imager::B( cimg ) ) / C
  H1[ , , 1, 1 ] = H1[ , , 1, 1 ] + ifelse( H1[ , , 1, 1 ] < 0, 6, 0 )
  H2 = ( imager::B( cimg ) - imager::R( cimg ) ) / C + 2
  H3 = ( imager::R( cimg ) - imager::G( cimg ) ) / C + 4
  hue[ M == imager::R( cimg ) ] = H1[ M == imager::R( cimg ) ]
  hue[ M == imager::G( cimg ) ] = H2[ M == imager::G( cimg ) ]
  hue[ M == imager::B( cimg ) ] = H3[ M == imager::B( cimg ) ]
  hue[ ( imager::R( cimg ) == imager::G( cimg ) ) & ( imager::R( cimg ) == imager::B( cimg ) ) ] = 0
  hue = hue * 60
  hue = hue %% 360
  # calculate L and S
  L = ( M + m ) / 2
  S = imager::cimg( array( 0, c( dim( cimg )[ 1:3 ], 1 ) ) )
  S = ( M - m ) / ( 1 - abs( M + m - 1 ) )
  cimg = imager::imappend( list( hue, S, L ), axis = "c" )
  return( cimg2nimg( cimg ) )
}


#' RGB to XYZ conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
RGB2XYZ = function( im, use.D65 = TRUE ){
  if( use.D65 ){
    X = 0.4124564 * get_R( im ) + 0.3575761 * get_G( im ) + 0.1804375 * get_B( im )
    Y = 0.2126729 * get_R( im ) + 0.7151522 * get_G( im ) + 0.0721750 * get_B( im )
    Z = 0.0193339 * get_R( im ) + 0.1191920 * get_G( im ) + 0.9503041 * get_B( im )
  } else {
    X = 0.4360747 * get_R( im ) + 0.3850649 * get_G( im ) + 0.1430804 * get_B( im )
    Y = 0.2225045 * get_R( im ) + 0.7168786 * get_G( im ) + 0.0606169 * get_B( im )
    Z = 0.0139322 * get_R( im ) + 0.0971045 * get_G( im ) + 0.7141733 * get_B( im )
  }
  return( merge_color( list( X, Y, Z ) ) )
}


#' XYZ to RGB conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
XYZ2RGB = function( im, use.D65 = TRUE ){
  if( use.D65 ){
    R =  3.24045484 * get_R( im ) - 1.5371389 * get_G( im ) - 0.49853155 * get_B( im )
    G = -0.96926639 * get_R( im ) + 1.8760109 * get_G( im ) + 0.04155608 * get_B( im )
    B =  0.05564342 * get_R( im ) - 0.2040259 * get_G( im ) + 1.05722516 * get_B( im )
  } else {
    R =  3.13385637 * get_R( im ) - 1.6168668 * get_G( im ) - 0.49061477 * get_B( im )
    G = -0.97876856 * get_R( im ) + 1.9161416 * get_G( im ) + 0.03345412 * get_B( im )
    B =  0.07194517 * get_R( im ) - 0.2289913 * get_G( im ) + 1.40524267 * get_B( im )
  }
  return( merge_color( list( R, G, B ) ) )
}


#' sRGB to XYZ conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
sRGB2XYZ = function( im, use.D65 = TRUE ){
  im %>% sRGB2RGB %>% RGB2XYZ( use.D65 )
}


#' XYZ to sRGB conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
XYZ2sRGB = function( im, use.D65 = TRUE ){
  im %>% XYZ2RGB( use.D65 ) %>% RGB2sRGB
}


#' XYZ to Lab conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
XYZ2Lab = function( im, use.D65 = TRUE ){
  # reference white
  if( use.D65 ){
    white = c( 0.95047, 1, 1.08883 )
  } else {
    white = c( 0.96420, 1, 0.82491 )
  }
  im[ ,,1 ] = im[ ,,1, drop = FALSE ] / white[ 1 ]
  im[ ,,3 ] = im[ ,,3, drop = FALSE ] / white[ 3 ]
  #
  mask = 24389 * im > 216
  im[ mask ] = im[ mask ]^( 1 / 3 )
  im[ !mask ] = ( 24389 * im[ !mask ] / 27 + 16 ) / 116
  fx = im[ ,,1, drop = FALSE ]
  fy = im[ ,,2, drop = FALSE ]
  fz = im[ ,,3, drop = FALSE ]
  #
  L = ( 116 * fy - 16 )
  a = 500 * ( fx - fy )
  b = 200 * ( fy - fz )
  return( merge_color( list( L, a, b ) ) )
}


#' Lab to XYZ conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
Lab2XYZ = function( im, use.D65 = TRUE ){
  eta = 216 / 24389
  kappa = 24389 / 27
  #
  fy = ( im[,,1, drop = FALSE ] + 16 ) / 116
  fx = 0.002 * im[,,2, drop = FALSE ] + fy
  fz = fy - 0.005 * im[,,3, drop = FALSE ]
  # x = fx^3 > eta ? fx^3 : ( 116 * fx - 16 ) / kappa
  mask = fx^3 > eta
  fx[ mask ] = fx[ mask ]^3
  fx[ !mask ] = ( 116 * fx[ !mask ] - 16 ) / kappa
  # y = L > 8 ? ( ( L + 16 ) / 116 )^3 : L / kappa
  L = im[,,1, drop = FALSE ]
  mask = L > 8
  L[ mask ] = ( ( L[ mask ] + 16 ) / 116 )^3
  L[ !mask ] = L[ !mask ] / kappa
  # z = fz^3 > eta ? fz^3 : ( 116 * fz - 16 ) / kappa
  mask = fz^3 > eta
  fz[ mask ] = fz[ mask ]^3
  fz[ !mask ] = ( 116 * fz[ !mask ] - 16 ) / kappa
  # reference white
  if( use.D65 ){
    white = c( 0.95047, 1, 1.08883 )
  } else {
    white = c( 0.96420, 1, 0.82491 )
  }
  fx = fx * white[ 1 ]
  fz = fz * white[ 3 ]
  return( merge_color( list( fx, L, fz ) ) )
}


#' sRGB to Lab conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
sRGB2Lab = function( im, use.D65 = TRUE ){
  XYZ2Lab( sRGB2XYZ( im, use.D65 ), use.D65 )
}


#' Lab to sRGB conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
Lab2sRGB = function( im, use.D65 = TRUE ){
  XYZ2sRGB( Lab2XYZ( im, use.D65 ), use.D65 )
}


#' RGB to Lab conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
RGB2Lab = function( im, use.D65 = TRUE ){
  im %>% RGB2XYZ( use.D65 ) %>% XYZ2Lab( use.D65 )
}


#' Lab to RGB conversion
#' @param im an image
#' @param use.D65 reference white, either TRUE (D65 is used) or FALSE (D50 is used).
#' @return an image
#' @export
Lab2RGB = function( im, use.D65 = TRUE ){
  im %>% Lab2XYZ( use.D65 ) %>% XYZ2RGB( use.D65 )
}


#' RGB to YUV conversion
#' @param im an image
#' @param use.B601 logical. Either TRUE (SDTV with BT.601) or FALSE (HDTV with BT.709).
#' @return an image
#' @source \url{ https://en.wikipedia.org/wiki/YUV }
#' @export
RGB2YUV = function( im, use.B601 = FALSE ){
  if( use.B601 ){
    Y =   0.299   * get_R( im ) + 0.587   * get_G( im ) + 0.114   * get_B( im )
    U = - 0.14713 * get_R( im ) - 0.28886 * get_G( im ) + 0.436   * get_B( im )
    V =   0.615   * get_R( im ) - 0.51499 * get_G( im ) - 0.10001 * get_B( im )
  } else {
    Y =   0.2126  * get_R( im ) + 0.7152  * get_G( im ) + 0.0722  * get_B( im )
    U = - 0.09991 * get_R( im ) - 0.33609 * get_G( im ) + 0.436   * get_B( im )
    V =   0.615   * get_R( im ) - 0.55861 * get_G( im ) - 0.05639 * get_B( im )
  }
  return( merge_color( list( Y, U, V ) ) )
}


#' YUV to RGB conversion
#' @param im an image
#' @param use.B601 logical. Either TRUE (SDTV with BT.601) or FALSE (HDTV with BT.709).
#' @return an image
#' @source \url{ https://en.wikipedia.org/wiki/YUV }
#' @export
YUV2RGB = function( im, use.B601 = FALSE ){
  if( use.B601 ){
    R = 1 * get_R( im ) + 0       * get_G( im ) + 1.13983 * get_B( im )
    G = 1 * get_R( im ) - 0.39465 * get_G( im ) - 0.58060 * get_B( im )
    B = 1 * get_R( im ) + 2.03211 * get_G( im ) + 0       * get_B( im )
  } else {
    R = 1 * get_R( im ) + 0       * get_G( im ) + 1.28033 * get_B( im )
    G = 1 * get_R( im ) - 0.21482 * get_G( im ) - 0.38059 * get_B( im )
    B = 1 * get_R( im ) + 2.12798 * get_G( im ) + 0       * get_B( im )
  }
  return( merge_color( list( R, G, B ) ) )
}


#' sRGB to YUV conversion
#' @param im an image
#' @param use.B601 logical. Either TRUE (SDTV with BT.601) or FALSE (HDTV with BT.709).
#' @return an image
#' @source \url{ https://en.wikipedia.org/wiki/YUV }
#' @export
sRGB2YUV = function( im, use.B601 = FALSE ){
  im %>% sRGB2RGB %>% RGB2YUV( use.B601 )
}


#' YUV to sRGB conversion
#' @param im an image
#' @param use.B601 logical. Either TRUE (SDTV with BT.601) or FALSE (HDTV with BT.709).
#' @return an image
#' @source \url{ https://en.wikipedia.org/wiki/YUV }
#' @export
YUV2sRGB = function( im,  use.B601 = FALSE ){
  im %>% YUV2RGB( use.B601 ) %>% RGB2sRGB
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Math ----


#' Rescale numeric vector to have a range between 0 to 1
#' @param x a numeric vector
#' @return a rescaled numeric vector
#' @seealso \code{\link{rescaling}}
#' @examples
#' rescaling01( 1:5 )
#' @export
rescaling01 = function( x ){
  if( max( x ) == min( x ) ){
    return( x )
  } else {
    return( ( x - min( x ) ) / ( max( x ) - min( x ) ) )
  }
}


#' Rescale numeric vector to have a specified range
#' @param x a numeric vector
#' @param from lowest value
#' @param to highest value
#' @return a rescaled numeric vector
#' @seealso \code{\link{rescaling01}}
#' @examples
#' rescaling( 1:5, from = 0, to = 10 )
#' @export
rescaling = function( x, from = 0, to = 1 ){
  if( max( x ) == min( x ) ){
    return( x )
  } else {
    return( from + ( to - from ) * rescaling01( x ) )
  }
}


#' Clamp values to a minimum and maximum value
#' @param x a numeric vector
#' @param min minimum value
#' @param max maximum value
#' @return a numeric vector
#' @examples
#' clamping( -5:5, min = -3, max = 3 )
#' @export
clamping = function( x, min = 0, max = 1 ){
  x[ x < min ] = min
  x[ x > max ] = max
  return( x )
}


#' Calculate a cubic spline
#' @param x a numeric vector
#' @param low minimum value of output
#' @param high maximum value of output
#' @return a numeric vector
#' @examples
#' x = seq( from = 0, to = 1, by = 0.01 )
#' plot( x, cubic_spline( x ) )
#' @export
cubic_spline = function( x, low = 0, high = 1 ){
  if( low == high ){
    warning( "low and high must be different!" )
  } else if( low > high ){
    return( 1 - ( cubic_spline( x, high, low ) ) )
  }
  x2 = x
  t = x[ x > low & x < high ]
  t = ( t - low ) / ( high - low )
  x2[ x > low & x < high ] = t^2 * ( 3 - 2 * t )
  x2[ x <= low ] = 0
  x2[ x >= high ] = 1
  return( x2 )
}


#' Calculate the Euclidean distance between two points
#' @param x1 x coordinate of point 1
#' @param y1 y coordinate of point 1
#' @param x2 x coordinate of point 2
#' @param y2 y coordinate of point 2
#' @return distance between point 1 and 2
#' @examples
#' eucdist(0, 0, 2, 2)
#' @export
eucdist = function( x1, y1, x2, y2 ){
  return( sqrt( ( x1 - x2 )^2 + ( y1 - y2 )^2 ) )
}


#' Shift operation
#' @param v a numeric vector
#' @param lag a numeric
#' @return a lagged vector
#' @examples
#' vec_shift(1:7, 2)
#' @export
vec_shift = function( v, lag = 0 ){
  if( lag == 0 || abs( lag ) == length( v ) ){
    return( v )
  }
  index = 1:length( v )
  lag = lag %% length( index )
  index = c( index[ length( index ) - lag + 1 ]:max( index ), 1:index[ length( index ) - lag ] )
  return( index )
}


#' Power operation
#' @param x a numeric vector
#' @param p power term
#' @return a numeric vector
#' @examples
#' pow(2, 3)
#' @export
pow = function( x, p ){
  return( x^p )
}


#' Calculate the range of values
#' @param x a numeric vector
#' @return the range of x
#' @examples
#' MinMax(1:10)
#' @export
MinMax = MaxMin = function( x ){
  return( max( x ) - min( x ) )
}


#' Apply a thresholding function
#'
#' For details, see http://dx.doi.org/10.1016/j.cag.2012.03.004
#' @param x a numeric vector
#' @param eta threshold
#' @param phi smoothing parameter
#' @return a numeric vector
#' @examples
#' x = seq( from = 0, to = 1, by = 0.01 )
#' plot(x, ramp_threshold( x, 0.5, 6 ))
#' @export
ramp_threshold = function( x, eta, phi ){
  y = x
  y[ x >= eta ] = 1
  y[ x < eta ] = 1 + tanh( phi * ( y[ x < eta ] - eta ) )
  return( y )
}


#' Generates a logarithmically spaced sequence
#' @param from starting value
#' @param to ending value
#' @param length.out number of intervening values
#' @return a numeric vector
#' @examples
#' logspace(10,100,10)
#' @export
logspace = function( from, to, length.out ){
  exp(seq(log(from), log(to), length.out = length.out))
}


#' Round floats to integers while preserving their sum
#' ref. https://stackoverflow.com/questions/32544646/round-vector-of-numerics-to-integer-while-preserving-their-sum
#' @param x a numeric vector
#' @param digits integer indicating the number of decimal places (round) to be used.
#' @return a numeric vector
#' @examples
#' smart.round(seq(1,1.5,0.05), 1)
#' @export
smart.round = function(x, digits = 1) {
  up = 10 ^ digits
  x = x * up
  y = floor(x)
  indices = utils::tail(order(x-y), round(sum(x)) - sum(y))
  y[indices] = y[indices] + 1
  y / up
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Geometry ----


#' Find the circle passing through three points
#'
#' Given 3 points, P1(x1, y1), P2(x2, y2) and P3(x3, y3), find the circle that passes through the points.
#' For details, see https://www.geeksforgeeks.org/equation-of-circle-when-three-points-on-the-circle-are-given/
#' @param x1 x coordinate of P1
#' @param y1 y coordinate of P1
#' @param x2 x coordinate of P2
#' @param y2 y coordinate of P2
#' @param x3 x coordinate of P3
#' @param y3 y coordinate of P3
#' @return a vector of 3 values, the center (x and y) and the radius of the circle
#' @examples
#' find_circle(0, 0, 2, 0, 0, 2)
#' @export
find_circle = function( x1, y1, x2, y2, x3, y3 ){
  x12 = x1 - x2
  x13 = x1 - x3
  y12 = y1 - y2
  y13 = y1 - y3
  y31 = y3 - y1
  y21 = y2 - y1
  x31 = x3 - x1
  x21 = x2 - x1

  sx13 = x1^2 - x3^2
  sy13 = y1^2 - y3^2
  sx21 = x2^2 - x1^2
  sy21 = y2^2 - y1^2

  f = ( sx13 * x12 + sy13 * x12 + sx21 * x13 + sy21 * x13 ) / ( 2 * ( y31 * x12 - y21 * x13 ) )
  g = ( sx13 * y12 + sy13 * y12 + sx21 * y13 + sy21 * y13 ) / ( 2 * ( x31 * y12 - x21 * y13 ) )
  c = -x1^2 - y1^2 - 2 * g * x1 - 2 * f * y1

  h = -g
  k = -f
  r = sqrt( h * h + k * k - c )

  return( c( x = h, y = k, r = r ) )
}


#' Check if a point is inside or outside an ellipse
#'
#' For details, see https://www.geeksforgeeks.org/check-if-a-point-is-inside-outside-or-on-the-ellipse/
#' @param x x coordinate of the point
#' @param y y coordinate of the point
#' @param h x coordinate of the ellipse center
#' @param k y coordinate of the ellipse center
#' @param a semi-major axis x
#' @param b semi-major axis y
#' @return mean squared error
#' @examples
#' is_inside_ellipse(x = 0, y = 0, h = 0, k = 0, a = 4, b = 2) #TRUE
#' is_inside_ellipse(x = 5, y = 0, h = 0, k = 0, a = 4, b = 2) #FALSE
#' @export
is_inside_ellipse = function( x, y, h, k, a, b ){
  p = ( (x - h)^2 / a^2 ) + ( (y - k)^2 / b^2 )
  return( p <= 1 )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Image creation ----

#' Create a monotone image
#' @param color single numeric for grayscale images, or three numeric values for RGB (color) images.
#' @param height image height
#' @param width image width
#' @return an image
#' @examples
#' plot(im_mono(0.5, 128, 128)) # a gray square
#' plot(im_mono(c(1, 0.2, 0.2), 64, 128)) # a red rectangle
#' @export
im_mono = function( color, height, width ){
  nc = length( color )
  if( ! nc %in% c( 1, 3 ) ){
    stop( "color length must be 1 or 3." )
  }
  dat = array( rep( color, each = height * width, times = nc), c( height, width, nc ) )
  return( nimg( dat ) )
}


#' Create a white noise image
#' @param height image height
#' @param width image width
#' @param n.channel number of color channel. either 1 (gras scale noise) or 3 (color noise)
#' @return an image
#' @examples
#' plot(im_noise(128, 128))
#' @export
im_noise = function( height, width, n.channel = 3 ){
  nimg( array( runif( height * width * n.channel ), c( height, width, n.channel ) ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Image info ----


#' Get image height
#' @param im an image
#' @return image's height
#' @examples
#' im_height(regatta)
#' @export
im_height = function( im ){
  dim( im )[ 1 ]
}


#' Get image width
#' @param im an image
#' @return image's width
#' @examples
#' im_width(regatta)
#' @export
im_width = function( im ){
  dim( im )[ 2 ]
}


#' Get image width and height
#' @param im an image
#' @return image's height
#' @examples
#' im_size(regatta)
#' @export
im_size = function( im ){
  unname( dim( im )[ 1:2 ] )
}


#' Get the diagonal length of an image
#' @param im an image
#' @return image's diagonal length
#' @examples
#' im_diagonal(regatta)
#' @export
im_diagonal = function( im ){
  sqrt( im_height( im )^2 + im_width( im )^2 )
}


#' Total number of pixels of an image
#' @param im an image
#' @return total number of pixels
#' @examples
#' im_npix(regatta)
#' @export
im_npix = function( im ){
  prod( dim( im ) )
}


#' The number of color channel of an image
#' @param im an image
#' @return the number of color channel
#' @examples
#' im_nchannel(regatta)
#' @export
im_nchannel = function( im ){
  dim( im )[ 3 ]
}


#' The number of color channel of an image
#' @param im an image
#' @return the number of color channel
#' @examples
#' im_nc(regatta)
#' @export
im_nc = function( im ){
  im_nchannel( im )
}


#' X coordinate of image center
#'
#' When the width of the image is an even number, digits after a decimal point are rounded up.
#' @param im an image
#' @return X coordinate of image center
#' @examples
#' im_cx(regatta)
#' @export
im_cx = function( im ){
  return( floor( im_width( im ) / 2 ) + 1 )
}


#' Y coordinate of image center
#'
#' When the height of the image is an even number, digits after a decimal point are rounded up.
#' @param im an image
#' @return Y coordinate of image center
#' @examples
#' im_cy(regatta)
#' @export
im_cy = function( im ){
  return( floor( im_height( im ) / 2 ) + 1 )
}


#' Get pixel coordinates for an image, as an image
#' @param im an image
#' @param direction Either "x" for x coordinates or "y" for y coordinates
#' @return an image containing pixel coordinates
#' @examples
#' xc = im_coord(regatta, "x")
#' plot(xc, rescale = TRUE)
#' @export
im_coord = function( im, direction = "x" ){
  if( direction == "x" ){
    return( nimg( array( rep( 1:im_width( im ), each = im_height( im ) ), dim = dim( im ) ) ) )
  } else if( direction == "y" ){
    return( nimg( array( rep( 1:im_height( im ), times = im_width( im ) ), dim = dim( im ) ) ) )
  } else {
    return( NULL )
  }
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Image slicing ----


force_channel_label_to_num = function( x ){
  if( is.numeric( x ) ){
    return( x )
  }
  y = c()
  for( i in 1:length( x ) ){
    if( x[ i ] %in% c( "R", "r", "L", "l" ) ){
      y = c( y, 1 )
    } else if( x[ i ] %in% c( "G", "g", "a" ) ){
      y = c( y, 2 )
    } else if( x[ i ] %in% c( "B", "b" ) ){
      y = c( y, 3 )
    } else if( x[ i ] %in% c( "A", "alpha", "Alpha" ) ){
      y = c( y, 4 )
    } else {
      y = c( y, 0 )
    }
  }
  return( y )
}


#' Extract a color channel from an image
#' @param im an image
#' @param channel color channel to extract. either numeric or string.
#' @return an image
#' @examples
#' R = get_channel(regatta, 1)
#' plot(R)
#' G = get_channel(regatta, "G")
#' plot(G)
#' @export
get_channel = function( im, channel ){
  if( length( dim( im ) ) == 2 ){
    return( im )
  } else {
    return( nimg( im[ , , force_channel_label_to_num( channel ), drop = FALSE ] ) )
  }
}


#' Extract the Red channel from an image
#' @param im an image
#' @return an image
#' @export
get_R = function( im ){
  return( get_channel( im, 1 ) )
}


#' Extract the Green channel from an image
#' @param im an image
#' @return an image
#' @export
get_G = function( im ){
  return( get_channel( im, 2 ) )
}


#' Extract the Blue channel from an image
#' @param im an image
#' @return an image
#' @export
get_B = function( im ){
  return( get_channel( im, 3 ) )
}


#' Extract the Alpha channel from an image
#' @param im an image
#' @return an image
#' @export
get_A = function( im ){
  return( get_channel( im, 4 ) )
}


#' Split color channel
#' @param im an image
#' @return a list of images
#' @examples
#' split_color(regatta) # a list of R, G, and B color channels
#' @export
split_color = function( im ){
  ls = list()
  for( i in 1:dim( im )[ 3 ] ){
    ls = c( ls, list( nimg( im[ , , i, drop = FALSE ] ) ) )
  }
  return( ls )
}


#' Merge a list of images into an image
#' @param imlist a list of images
#' @return an image
#' @examples
#' merge_color(split_color(regatta))
#' @export
merge_color = function( imlist ){
  imdim = dim( imlist[[ 1 ]] )
  im = array( 0, c( imdim[ 1 ], imdim[ 2 ], length( imlist ) ) )
  for( i in 1:length( imlist ) ){
    im[,,i] = imlist[[ i ]]
  }
  return( nimg( im ) )
}


#' Pile two images
#' @param im an image
#' @param im2 an image
#' @return an image
#' @examples
#' plot(im_pile(regatta, im_gray(regatta)))
#' @export
im_pile = function( im, im2 ){
  img = nimg( array( 0, c( im_height( im ), im_width( im ), im_nchannel( im ) + im_nchannel( im2 ) ) ) )
  img[, , 1:im_nchannel( im ) ] = im
  img[, , ( 1 + im_nchannel( im ) ):im_nchannel( img ) ] = im2
  return( img )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Alpha ----

#' Extract the alpha channel from an image
#' @param im an image
#' @return an image
#' @export
get_alpha = function( im ){
  return( get_channel( im, 4 ) )
}


#' Remove a channel(s) from an image
#' @param im an image
#' @param index channel index to be removed
#' @return an image
#' @examples
#' plot(remove_channel(regatta, 1:2)) # remove 1st (R) and 2nd (G) channels
#' @export
remove_channel = function( im, index ){
  im2 = split_color( im )
  im2[ index ] = NULL
  im3 = merge_color( im2 )
  return( im3 )
}


#' Remove alpha channel from an image
#' @param im an image
#' @param index alpha channel index. 4 for RGB image, 2 for grayscale image.
#' @return an image
#' @export
remove_alpha = function( im, index ){
  if( missing( index ) ){
    if( im_nc( im ) %in% c( 2, 4 ) ){
      index = im_nc( im )
    } else {
      stop( "index must be specified." )
    }
  }
  return( remove_channel( im, index ) )
}


#' Alpha blending of two images
#' @param im an image (foreground)
#' @param im2 an image (background)
#' @param alpha an image (alpha channel). if missing, the alpha channel of im is used.
#' @return an image
#' @export
im_blend = function( im, im2, alpha ){
  if( missing( alpha ) ){
    alpha = get_A( im ) # 0 = background, 1 = object
  }
  im3 = ( 1 - im_tricolored( alpha ) ) * im_tricolored( im2 ) + im_tricolored( alpha ) * im_tricolored( im )
  return( im3 )
}


#' Add a fading border to an image
#' @param im an image
#' @param npix thickness of the fading border
#' @param blur_radius apply a blur of this radius to the fading border to avoid illusory edges at the image corners
#' @return an image
#' @examples
#' plot(fade_border(regatta, npix = 30))
#' @export
fade_border = function( im, npix, blur_radius = 6 ){
  im_ = im

  margin = 1
  npix = max( blur_radius / 2, npix)
  blur_radius = min( npix - margin * 2, blur_radius )
  shade = seq( from = 0, to = 1, length.out = npix + 2 - ( max( margin, blur_radius - margin ) ) ) %>%
    cubic_spline
  # append some margin zeros to the outer side of the shade spline to avoid faint outer borders to appear
  shade = c( rep( 0, max( margin, blur_radius - margin ) ), shade )
  # remove the beginning (0) and the end (1) of the spline
  shade = shade[ 2:( length(shade) - 1 ) ]

  im = nimg(array(1,c(im_height(im), im_width(im),1))) %>% nimg2cimg
  im[ c(1, imager::width(im)), , 1, 1]  = 0
  im[ , c(1, imager::height(im)), 1, 1] = 0
  mask = im
  im[ c(1, imager::width(im)), , 1, 1]  = shade[ 1 ]
  im[ , c(1, imager::height(im)), 1, 1] = shade[ 1 ]
  for( i in 2:npix ){
    mask = imager::shrink( mask, 3 )
    im[ im == 1 & !mask ] = shade[ i ]
  }
  alpha = cimg2nimg( im ) %>% get_R %>% box_blur( blur_radius ) %>% clamping
  out = im_pile( im_, alpha )
  return( out )
}
# fade_border(regatta %>% im_resize( 240 ), 20, 0) %>% plot() #%>% im_save( "im_fade", getwd() )


#' Add a fading border to an image region
#' @param im an image
#' @param mask an image
#' @param npix thickness of the fading border
#' @param blur_radius apply a blur of this radius to the fading border to avoid illusory edges at the image corners
#' @return an image
#' @examples
#' im = regatta
#' mask = im_mono(0, im_height(im), im_width(im))
#' mask[ 100:350, 100:500,] = 1
#' plot(fade_mask(im, mask, 20))
#' @export
fade_mask = function( im, mask, npix, blur_radius = 3 ){
  im_ = im_tricolored( im )
  mask = get_R( mask )
  mask[ mask < 1 ] = 0

  margin = 1
  npix = max( blur_radius / 2, npix)
  blur_radius = min( npix - margin * 2, blur_radius )
  shade = seq( from = 0, to = 1, length.out = npix + 2 - ( max( margin, blur_radius - margin ) ) ) %>%
    cubic_spline
  # append some margin zeros to the outer side of the shade spline to avoid faint outer borders to appear
  shade = c( rep( 0, max( margin, blur_radius - margin ) ), shade )
  # remove the beginning (0) and the end (1) of the spline
  shade = shade[ 2:( length(shade) - 1 ) ]

  im = mask %>% nimg2cimg
  im2 = mask %>% nimg2cimg
  for( i in 1:npix ){
    im2 = imager::shrink( im2, 3 )
    im[ im == 1 & !im2 ] = shade[ i ]
  }
  alpha = cimg2nimg( im ) %>% get_R %>% box_blur( blur_radius ) %>% clamping
  out = im_pile( im_, alpha )
  return( out )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Image transform ----


#' Reverse pixel values
#' @param im an image
#' @return an image
#' @examples
#' plot(im_reverse(regatta))
#' @export
im_reverse = function( im ){
  return( 1 - im )
}


#' Replicate a channel along color dimension
#' @param im an image
#' @param n number of repeat
#' @param channel which channel to extract
#' @return an image
#' @export
im_rep = function( im, n = 3, channel = 1 ){
  nimg( array( get_channel( im, channel ), c( im_height( im ), im_width( im ), n ) ) )
}


#' Force an image to have three and only three color channels
#' @param im an image
#' @return an image
#' @examples
#' R = get_R(regatta)
#' im = im_tricolored(R)
#' @export
im_tricolored = function( im ){
  n = im_nc( im )
  if( n < 3 ){
    return( im_rep( im, 3, 1 ) )
  } else if( n > 3 ){
    return( get_channel( im, 1:3 ) )
  } else {
    return( im )
  }
}


#' Pad image with n pixels
#' @param im an image
#' @param n number of pixels to pad with
#' @param method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @examples
#' # zero padding
#' im = im_pad(regatta, 20, "zero")
#' plot(im)
#' # mirror padding
#' im = im_pad(regatta, 100, "mirror")
#' plot(im)
#' @export
im_pad = function( im, n, method = "mirror" ){
  if( n == 0 ) return( im )

  w = im_width( im )
  h = im_height( im )

  if( any( n > c( w, h ) ) ){
    warning( "n must be equal or smaller than image width (and height)." )
    return( im )
  }

  # create an empty matrix
  x = ifelse( is.numeric( method ), method, ifelse( method == "mean", mean( im ), 0 ) )
  mat = array( x, c( h + 2 * n, w + 2 * n, dim( im )[ 3 ] ) )

  # put the image
  mat[ ( n + 1 ):( n + h ), ( n + 1 ):( n + w ), ] = im

  # padding
  if( method == "zero" || method == "mean" || is.numeric( method ) ){
    # do nothing
  } else if( method == "repeat" ){
    # top left
    mat[ 1:n, 1:n, ] = im[ (h-n+1):h, (w-n+1):w, ]
    # top
    mat[ 1:n, (n+1):(n+w), ] = im[ (h-n+1):h, 1:w, ]
    # top right
    mat[ 1:n, (n+w+1):(2*n+w), ] = im[ (h-n+1):h, 1:n, ]
    # left
    mat[ (n+1):(n+h), 1:n, ] = im[ 1:h, (w-n+1):w, ]
    # right
    mat[ (n+1):(n+h), (n+w+1):(2*n+w), ] = im[ 1:h, 1:n, ]
    # bottom left
    mat[ (n+h+1):(2*n+h), 1:n, ] = im[ 1:n, (w-n+1):w, ]
    # bottom
    mat[ (n+h+1):(2*n+h), (n+1):(n+w), ] = im[ 1:n, 1:w, ]
    # bottom right
    mat[ (n+h+1):(2*n+h), (n+w+1):(2*n+w), ] = im[ 1:n, 1:n, ]
  } else if( method == "mirror" ){
    # top left
    mat[ 1:n, 1:n, ] = im[ n:1, n:1, ]
    # top
    mat[ 1:n, (n+1):(n+w), ] = im[ n:1, 1:w, ]
    # top right
    mat[ 1:n, (n+w+1):(2*n+w), ] = im[ n:1, w:(w-n+1), ]
    # left
    mat[ (n+1):(n+h), 1:n, ] = im[ 1:h, n:1, ]
    # right
    mat[ (n+1):(n+h), (n+w+1):(2*n+w), ] = im[ 1:h, w:(w-n+1), ]
    # bottom left
    mat[ (n+h+1):(2*n+h), 1:n, ] = im[ h:(h-n+1), n:1, ]
    # bottom
    mat[ (n+h+1):(2*n+h), (n+1):(n+w), ] = im[ h:(h-n+1), 1:w, ]
    # bottom right
    mat[ (n+h+1):(2*n+h), (n+w+1):(2*n+w), ] = im[ h:(h-n+1), w:(w-n+1), ]
  }

  im = nimg( mat )
  return( im )
}


#' Image shift operation
#' @param im an image
#' @param axis either "x" or "y"
#' @param lag a numeric
#' @return an image
#' @examples
#' # shift a image 100 pixels to the right
#' im = im_shift(regatta, axis = "x", lag = 100)
#' plot(im)
#' @export
im_shift = function( im, axis = "x", lag = 0 ){
  if( axis == "x" ){
    index = vec_shift( 1:dim(im)[2], lag )
    im = im[ ,index, , drop = FALSE ]
  } else if( axis == "y" ){
    index = vec_shift( 1:dim(im)[1], lag )
    im = im[ index, , , drop = FALSE ]
  }
  return( nimg( im ) )
}




#' Image cropping
#' @param im an image
#' @param margin a numeric vector.
#' @return an image
#' @examples
#' plot(im_crop(regatta, 100)) # crop all four sides with the same value
#' plot(im_crop(regatta, c(100, 50))) # vertical, horizontal
#' plot(im_crop(regatta, c(0, 50, 50, 200))) # top, right, bottom, left
#' @export
im_crop = function( im, margin ){
  if( length( margin ) == 1 ){
    top = bottom = left = right = margin
  } else if( length( margin ) == 2 ){
    top = bottom = margin[ 1 ]
    left = right = margin[ 2 ]
  } else if( length( margin ) == 3 ){
    warning( "margin length must be 1, 2, or 4!" )
  } else if( length( margin ) == 4 ){
    top = margin[ 1 ]
    right = margin[ 2 ]
    bottom = margin[ 3 ]
    left = margin[ 4 ]
  }
  im = im[ (1 + top):(im_height( im ) - bottom), (1 + left):(im_width( im ) - right), , drop = FALSE ]
  return( nimg( im ) )
}


#' Image cropping
#' @param im an image
#' @param y1 y coordinate of top-left corner
#' @param x1 x coordinate of top-left corner
#' @param y2 y coordinate of bottom-right corner
#' @param x2 x coordinate of bottom-right corner
#' @return an image
#' @examples
#' plot(im_get(regatta, 1, 1, 100, 100))
#' @export
im_get = function( im, y1, x1, y2, x2 ){
  im = im[ y1:y2, x1:x2, , drop = FALSE ]
  return( nimg( im ) )
}


#' Make an image square shaped
#' @param im an image
#' @param position square center. a numeric value between 0 and 1.
#' @return an image
#' @examples
#' plot(im_crop_square(regatta))
#' plot(im_crop_square(regatta, position = 0))
#' plot(im_crop_square(regatta, position = 0.8))
#' @export
im_crop_square = function( im, position = 0.5 ){
  position = clamping( position )
  diff = im_width( im ) - im_height( im )
  position = 2 * position - 1 # range [-1,1]
  size = min( im_size( im ) )
  erode = abs( diff ) / 2
  center = max( im_size( im ) ) / 2
  start = floor( center - size / 2 + erode * position )
  if( start < 1 ) start = 1
  end = start + size - 1
  if( diff > 0 ){ # wide
    im = im_crop( im, c( 0, im_width( im ) - end, 0, start - 1 ) )
  } else { # tall
    im = im_crop( im, c( start - 1, 0, im_height( im ) - end, 0 ) )
  }
  return( nimg( im ) )
}


#' Rotate an image
#' @param im an image
#' @param angle rotation angle in degrees
#' @param expand either FALSE (default, does not change image size) or TRUE
#' @param cx center of rotation along x
#' @param cy center of rotation along y
#' @param interpolation Type of interpolation. Either 0 (nearest), 1 (linear), or 2 (cubic).
#' @param pad Type of padding. Either "zero", "neumann", or "repeat".
#' @return an image
#' @examples
#' plot(im_rotate(regatta, 30))
#' plot(im_rotate(regatta, 30, expand = TRUE))
#' plot(im_rotate(regatta, 30, expand = TRUE, pad = "repeat"))
#' @export
im_rotate = function(im, angle, expand = FALSE, cx = NULL, cy = NULL, interpolation = 2, pad = "zero"){
  cimg = nimg2cimg( im )
  boundary = 0
  if( pad == "neumann" ){
    boundary = 1
  } else if( pad == "repeat" ){
    boundary = 2
  }
  if( is.null( cx ) && is.null( cy ) ){
    if( expand ){
      im = imager::imrotate( cimg, angle, interpolation = interpolation, boundary = boundary )
    } else {
      im = imager::imrotate( cimg, angle, im_width(im)/2, im_height(im)/2, interpolation, boundary )
    }
  } else if( ! is.null( cx ) && ! is.null( cy ) ){
    im = imager::imrotate( cimg, angle, cx, cy, interpolation, boundary )
  } else {
    warning( "You must specify both cx and cy." )
    return( NULL )
  }
  return( cimg2nimg( clamping( im ) ) )
}


#' Flip an image
#' @param im an image
#' @param direction Either "horizontal" (default) or "vertical".
#' @return an image
#' @examples
#' plot(im_flip(regatta, "horizontal"))
#' plot(im_flip(regatta, "vertical"))
#' @export
im_flip = function( im, direction = "horizontal" ){
  if( direction == "horizontal" ){
    im[ , im_width( im ):1, ] = im
    return( im )
  } else if( direction == "vertical" ){
    im[ im_height( im ):1, , ] = im
    return( im )
  } else {
    return( NULL )
  }
}


#' Flip an image
#' @param im an image
#' @param direction Either "horizontal" (default) or "vertical".
#' @param position where to flip the image
#' @return an image
#' @export
im_flip_position = function( im, direction = "horizontal", position ){
  if( direction == "horizontal" ){
    if( missing( position ) ){
      im[ , im_width( im ):1, ] = im
    } else {
      center = ( im_width( im ) %/% 2 ) + 1
      lag = center - position
      im = im_shift( im, "x", lag )
      im[ , im_width( im ):1, ] = im
      im = im_shift( im, "x", -lag )
    }
    return( im )
  } else if( direction == "vertical" ){
    if( missing( position ) ){
      im[ im_height( im ):1, , ] = im
    } else {
      center = ( im_height( im ) %/% 2 ) + 1
      lag = center - position
      im = im_shift( im, "y", lag )
      im[ im_height( im ):1, , ] = im
      im = im_shift( im, "y", -lag )
    }
    return( im )
  } else {
    return( NULL )
  }
}


#' Resize image
#'
#' If either height or width is given, the other is determined to keep the aspect ratio of image.
#' @param im an image
#' @param height image height
#' @param width image width
#' @param interpolation Interpolation method. Either 0 (nearest-neighbor), 1 (linear), or 2 (cubic).
#' @return an image
#' @examples
#' dim(regatta)
#' dim(im_resize(regatta, height = 150))
#' dim(im_resize(regatta, width = 300))
#' dim(im_resize(regatta, height = 100, width = 100))
#' @export
im_resize = function( im, height, width, interpolation = 1 ){
  itype = 1 + 2 * interpolation # 0->1, 1->3, 2->5
  if( base::missing( width ) ){ # scale to height
    width = round( im_width( im ) * ( height / im_height( im ) ) )
  } else if( base::missing( height ) ){ # scale to width
    height = round( im_height( im ) * ( width / im_width( im ) ) )
  }
  im = imager::resize( nimg2cimg( im ), size_x = width, size_y = height, interpolation_type = itype )
  return( cimg2nimg( im ) )
}


#' Resize image
#' @param im an image
#' @param bound max image size (width/height)
#' @param interpolation Interpolation method. Either 0 (nearest-neighbor), 1 (linear), or 2 (cubic).
#' @return an image
#' @examples
#' dim(regatta)
#' dim(im_resize_limit(regatta, 200))
#' @export
im_resize_limit = function( im, bound, interpolation = 1 ){
  if( max( im_size( im ) ) < bound ){
    return( im )
  }
  if( im_width( im ) > im_height( im ) ){
    im_resize( im, width = bound, interpolation = interpolation )
  } else {
    im_resize( im, height = bound, interpolation = interpolation )
  }
}


#' Resize image
#' @param im an image
#' @param bound max image size (width/height)
#' @param interpolation Interpolation method. Either 0 (nearest-neighbor), 1 (linear), or 2 (cubic).
#' @return an image
#' @examples
#' dim(regatta)
#' dim(im_resize_limit_min(regatta, 200))
#' @export
im_resize_limit_min = function( im, bound, interpolation = 1 ){
  if( min( im_size( im ) ) <= bound ){
    return( im )
  }
  if( im_width( im ) > im_height( im ) ){
    im_resize( im, height = bound, interpolation = interpolation )
  } else {
    im_resize( im, width = bound, interpolation = interpolation )
  }
}


#' Resize image by a scale factor
#' @param im an image
#' @param scale a scale factor
#' @param interpolation Interpolation method. Either 0 (nearest-neighbor), 1 (linear), or 2 (cubic).
#' @return an image
#' @examples
#' dim(regatta)
#' dim(im_resize_scale(regatta, 0.5)) # half size
#' @export
im_resize_scale = function( im, scale = 1, interpolation = 1 ){
  itype = 1 + 2 * interpolation # 0->1, 1->3, 2->5
  im = imager::imresize( nimg2cimg( im ), scale, itype )
  return( cimg2nimg( im ) )
}


#' Combine images
#' @param im1 an image
#' @param im2 an image
#' @param y y-offset
#' @param x x-offset
#' @param alpha either FALSE (default) or TRUE (enable alpha transparency)
#' @param background background color
#' @return an image
#' @examples
#' plot(im_combine(regatta, regatta, y = im_height(regatta)))
#' plot(im_combine(regatta, regatta, y = 100, x = 200))
#' plot(im_combine(regatta, regatta, y = 100, x = 200, background = 0.5))
#' plot(im_combine(regatta, regatta, y = 100, x = 200, background = c(1, 0.5, 0.5)))
#' @export
im_combine = function( im1, im2, y = 0, x = 0, alpha = FALSE, background = 1 ){
  cc = max( im_nc( im1 ), im_nc( im2 ) )
  h = max( im_height( im1 ), y + im_height( im2 ), im_height( im2 ), - y + im_height( im1 ) )
  w = max( im_width( im1 ), x + im_width( im2 ), im_width( im2 ), - x + im_width( im1 ) )
  im = array( rep( background, each = h * w, times = cc ), dim = c( h, w, cc ) )

  y1 = ifelse( y < 0, -y, 0 ) + 1
  y2 = ifelse( y < 0, 0, y ) + 1
  x1 = ifelse( x < 0, -x, 0 ) + 1
  x2 = ifelse( x < 0, 0, x ) + 1
  im[ y1:( y1 + im_height( im1 ) - 1 ), x1:( x1 + im_width( im1 ) - 1 ), 1:cc ] = im1
  im[ y2:( y2 + im_height( im2 ) - 1 ), x2:( x2 + im_width( im2 ) - 1 ), 1:cc ] = im2
  if( ! alpha ){
    return( nimg( im ) )
  } else {
    A = array( 0, dim = c( h, w, 1 ) )
    A[ y1:( y1 + im_height( im1 ) - 1 ), x1:( x1 + im_width( im1 ) - 1 ), 1 ] = 1
    A[ y2:( y2 + im_height( im2 ) - 1 ), x2:( x2 + im_width( im2 ) - 1 ), 1 ] = 1
    return( merge_color( c( split_color( im ), list( A ) ) ) )
  }
}


#' Threshold grayscale image
#' @param im an image
#' @param thr a threshold
#' @param approx skip
#' @param adjust adjust the automatic threshold
#' @return an image
#' @examples
#' plot(im_threshold(get_R(regatta), thr = 0.6))
#' @export
im_threshold = function( im, thr = "auto", approx = TRUE, adjust = 1 ){
  cimg2nimg( imager::threshold( nimg2cimg( im ), thr, approx, adjust ) )
}


#' Raise pixel values
#' @param im an image
#' @param intercept a numeric value between 0 and 1
#' @return an image
#' @examples
#' plot(im_raise(regatta, 0.3))
#' @export
im_raise = function( im, intercept ){
  intercept + ( 1 - intercept ) * im
}


#' Apply a mosaic effect
#' @param im an image
#' @param size box size in pixel (default is 16)
#' @return an image
#' @examples
#' plot(im_pixelate(regatta, size = 16))
#' @export
im_pixelate = function( im, size = 16 ){
  if( size == 1 ){
    return( im )
  } else {
    im2 = im_resize_scale( im, scale = 1 / size, interpolation = 2 )
    im2 = im_resize( im2, im_height( im ), im_width( im ), interpolation = 0 )
    return( im2 )
  }
}


#' Apply a mosaic effect followed by position randomization
#' @param im an image
#' @param size box size in pixel (default is 16)
#' @param direction shuffle direction (either "x", "y", or "xy"). default is "xy".
#' @param noise percent of noise (default is 1). a numeric value between 0 and 1.
#' @return an image
#' @examples
#' plot(im_random_pixelate(regatta, size = 16, direction = "xy", noise = 0.1))
#' @export
im_random_pixelate = function( im, size = 16, direction = "xy", noise = 1 ){
  if( size == 1 ){
    if( direction %in% c( "x", "xy", "yx", "both" ) ){
      for( y in 1:im_height( im ) ){
        # im[ y, , ] = im[ y, sample( im_width( im ) ), ] # horizontal randomization
        sample_index = sample( im_width( im ), round( im_width( im ) * noise ) )
        index = 1:im_width( im )
        if( length( sample_index ) == 1 ){
          index[ sample_index ] = sample_index
        } else {
          index[ sample_index ] = sample( sample_index )
        }
        im[ y, , ] = im[ y, index, ] # horizontal randomization
      }
    }
    if( direction %in% c( "y", "xy", "yx", "both" ) ){
      for( x in 1:im_width( im ) ){
        # im[ ,x , ] = im[ sample( im_height( im ) ), x, ] # vertical randomization
        sample_index = sample( im_height( im ), round( im_height( im ) * noise ) )
        index = 1:im_height( im )
        if( length( sample_index ) == 1 ){
          index[ sample_index ] = sample_index
        } else {
          index[ sample_index ] = sample( sample_index )
        }
        im[ ,x , ] = im[ index, x, ] # vertical randomization
      }
    }
    return( im )
  } else {
    im2 = im_resize_scale( im, scale = 1 / size, interpolation = 1 )
    im2 = im_random_pixelate( im2, size = 1, direction, noise )
    im2 = im_resize( im2, im_height( im ), im_width( im ), interpolation = 0 )
    return( im2 )
  }
}


#' Apply a pixel randomization effect
#' @param im an image
#' @param size box size in pixel (default is 1)
#' @param direction shuffle direction (either "x", "y", or "xy"). default is "xy".
#' @param shuffle shuffle along the direction. either TRUE (default) or FALSE
#' @param fraction a fraction of stripes are shuffled. a numeric value between 0 and 1 (default is 1).
#' @param noise percent of noise. a numeric value between 0 and 1 (default is 1).
#' @return an image
#' @examples
#' plot(im_randomize(regatta, size = 16, direction = "xy", shuffle = TRUE, fraction = 1, noise = 1))
#' @export
im_randomize = function( im, size = 16, direction = "xy", shuffle = TRUE, fraction = 1, noise = 1 ){
  if( direction %in% c( "xy", "yx", "both" ) ){
    im2 = im_randomize( im, size, "x", shuffle, fraction, noise )
    im2 = im_randomize( im2, size, "y", shuffle, fraction, noise )
    return( im2 )
  } else if( direction %in% c( "x" ) ){
    # crop the image to fit the multiples of squares
    h1 = im_height( im )
    w1 = im_width( im )
    h2 = im_height( im ) - im_height( im ) %% size
    w2 = im_width( im ) - im_width( im ) %% size
    dh = h1 - h2
    dw = w1 - w2
    im = im_crop( im, margin = c( floor( dh/2 ), floor( dw/2 ), ceiling( dh/2 ), ceiling( dw/2 ) ) )
    #
    im2 = im_rotate( im, 90, TRUE )
    im2 = im_randomize( im2, size, "y", shuffle, fraction, noise )
    im2 = im_rotate( im2, -90, TRUE )
    return( im2 )
  } else if( direction %in% c( "y" ) ){
    # crop the image to fit the multiples of squares
    h1 = im_height( im )
    w1 = im_width( im )
    h2 = im_height( im ) - im_height( im ) %% size
    w2 = im_width( im ) - im_width( im ) %% size
    dh = h1 - h2
    dw = w1 - w2
    im = im_crop( im, margin = c( floor( dh/2 ), floor( dw/2 ), ceiling( dh/2 ), ceiling( dw/2 ) ) )
    # shuffle image stripes
    sample_index = seq( from = 1, to = im_height( im ) - size + 1, by = size )
    sample_index = sample( sample_index, round( length( sample_index ) * fraction ) )
    for( i in 1:size ){
      if( i == 1 ){
        sid = sample_index
      } else {
        sid = c( sid, sample_index + ( i - 1 ) )
      }
    }
    for( i in 1:size ){
      if( i == 1 ){
        if( shuffle ){
          sample_index = sample( sample_index )
        }
        sid2 = sample_index
      } else {
        sid2 = c( sid2, sample_index + ( i - 1 ) )
      }
    }
    index = 1:im_height( im )
    index[ sid ] = sid2
    im[ , , ] = im[ index, , ]
    # shuffle a fraction of pixel boxes for each stripe
    sample_index2 = sample_index
    for( y in sample_index2 ){
      sample_index = seq( from = 1, to = im_width( im ) - size + 1, by = size )
      sample_index = sample( sample_index, round( length( sample_index ) * noise ) )
      for( i in 1:size ){
        if( i == 1 ){
          sid = sample_index
        } else {
          sid = c( sid, sample_index + ( i - 1 ) )
        }
      }
      for( i in 1:size ){
        if( i == 1 ){
          if( length( sample_index ) == 1 ){
            sid2 = sample_index
          } else {
            sample_index = sample( sample_index )
            sid2 = sample_index
          }
        } else {
          sid2 = c( sid2, sample_index + ( i - 1 ) )
        }
      }
      index = 1:im_width( im )
      index[ sid ] = sid2
      im[ y:(y+size-1), , ] = im[ y:(y+size-1), index, ]
    }
    return( im )
  } else {
    return( NULL )
  }
}


# plot(im_glitch(regatta))
im_glitch = function( im ){
  # im = face
  # im = im_glitch( face )
  # im = im_random_mosaic( face, 16, noise = 0.1 )
  R = im_shift( get_R( im ), axis = "x", lag =  6 )
  B = im_shift( get_B( im ), axis = "x", lag = -6 )
  G = im_shift( get_G( im ), axis = "y", lag =  0 )
  im2 = merge_color( list( R, G, B ) )
  plot( im2 )
  im %>%
    im_randomize( size = 64, "y", F, .4, 0.4 ) %>%
    im_randomize( size = 32, "y", F, .2, 0.2 ) %>%
    im_randomize( size = 16, "y", F, .4, 0.1 ) %>%
    im_randomize( size =  8, "y", F, .6, 0.03 ) %>%
    im_randomize( size =  2, "y", F, .4, 0.02 ) %>%
    im_randomize( size =  5, "y", F, .03, 0.4 ) %>%
    return
}


#' Add an oval mask to an image
#' @param im an image
#' @param color  Color of the margin area
#' @examples
#' plot(apply_oval_mask(regatta, 0.5))
#' plot(apply_oval_mask(regatta, c(1,1,0.2)))
#' @export
apply_oval_mask = function( im, color = 0.5 ){
  h = im_height( im )
  w = im_width( im )
  mask = get_oval_mask( h, w, h / 2, w / 2, h / 2, w / 2 )
  mask = im_rep( mask, n = 3 )
  im = im_tricolored( im )
  if( length( color ) == 1 ){
    color = rep( color, 3 )
  }
  im[ mask == 0 ] = im_mono( color, h, w )[ mask == 0 ]
  return( im )
}


#' Convert an image to a numeric vector
#' Pixel values of R channel comes first, and then G, and finally B.
#' Each channel is serialized vertically (column-wise).
#' @param im an image
#' @examples
#' x = im_serialize(regatta)
#' @export
im_serialize = function( im ){
  return( c( array( get_R(im) ), array( get_G(im) ), array( get_B(im) ) ) )
}


#' Convert a serialized image to an image
#' @param im serialized image array
#' @param height image height
#' @param width image width
#' @param nc number of channels
#' @examples
#' se = im_serialize(regatta)
#' im = im_serialize_inv(se, 450, 600)
#' sum((regatta - im)^2)
#' @export
im_serialize_inv = function( im, height, width, nc = 3 ){
  n = length(im) / nc
  out = array( -1, dim = c(height, width, nc ) )
  for( i in 1:nc ){
    out[,,i] = matrix( im[(1+(i-1)*n):(i*n)], height, width )
  }
  return( nimg( out ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Luminance ----


#' Convert to grayscale
#' @param im an image
#' @param tricolored if TRUE, returned image has three color channels
#' @return an image
#' @examples
#' plot(im_gray(regatta))
#' @export
im_gray = function( im, tricolored = FALSE ){
  if( im_nc( im ) < 2 ){
    return( im )
  }
  lab = sRGB2Lab( im )
  L = get_R( lab )
  C0 = array( 0, dim = dim( L ) )
  im = merge_color( list( L, C0, C0 ) ) %>% Lab2sRGB
  if( ! tricolored ){
    im = get_R( im )
  }
  im = clamping( im )
  return( im )
}


#' Get L channel of CIELAB color space
#' @param im an image
#' @param scale if TRUE (default), L value is divided by 100
#' @return an image
#' @examples
#' plot(get_L(regatta))
#' @export
get_L = function( im, scale = TRUE ){
  if( im_nc( im ) == 1 ){
    return( im )
  } else if( im_nc( im ) == 2 ){
    return( get_R( im ) )
  }
  if( scale ){
    return( get_R( sRGB2Lab( im ) ) / 100 )
  } else {
    return( get_R( sRGB2Lab( im ) ) )
  }
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Masking ----


#' Create an arc mask image
#' @param height image height
#' @param width image width
#' @param cy arc center y
#' @param cx arc center x
#' @param r arc radius
#' @param t1 angle (start) in radius
#' @param t2 angle (end) in radius
#' @return an image
#' @examples
#' mask = get_arc_mask( 100, 120, 50, 50, 20, pi/6, pi )
#' plot(mask)
#' @export
get_arc_mask = function( height, width, cy, cx, r, t1, t2){
  maskD = matrix( 0, nrow = height, ncol = width )
  u = matrix( 1:width - cx, ncol = width, nrow = height, byrow = T )
  v = matrix( 1:height - cy, ncol = width, nrow = height, byrow = F )
  D = sqrt( u^2 + v^2 )
  maskD[ D <= r ] = 1

  maskA = matrix( 0, nrow = height, ncol = width )
  A = -atan2( v, u )
  A[ A < 0 ] = A[ A < 0 ] + 2 * pi
  maskA[ A >= min(t1,t2) & A <= max(t1,t2) ] = 1

  maskA[ maskD == 0 ] = 0
  return( nimg( maskA ) )
}


#' Create a circular mask image
#' @param height image height
#' @param width image width
#' @param cy circle center y
#' @param cx circle center x
#' @param r circle radius
#' @return an image
#' @examples
#' mask = get_circular_mask( 100, 120, 50, 50, 20 )
#' plot(mask)
#' @export
get_circular_mask = function( height, width, cy, cx, r ){
  v = matrix( 1:height - cy, ncol = width, nrow = height, byrow = FALSE )
  u = matrix( 1:width - cx, ncol = width, nrow = height, byrow = TRUE )
  D = u^2 + v^2
  im = matrix( TRUE, nrow = height, ncol = width )
  im[ D > r^2 ] = FALSE
  return( nimg( im ) )
}


#' Create a oval mask image
#' @param height image height
#' @param width image width
#' @param cy oval center y
#' @param cx oval center x
#' @param ry oval radius y
#' @param rx oval radius x
#' @return an image
#' @examples
#' mask = get_oval_mask(100, 200, 50, 10, 20, 40 )
#' plot(mask)
#' @export
get_oval_mask = function( height, width, cy, cx, ry, rx ){
  im = nimg( array( 0, c( height, width, 1 ) ) )
  yy = im_coord( im, "y" )
  xx = im_coord( im, "x" )
  D = ( xx - cx )^2 / rx^2 + ( yy - cy )^2 / ry^2 <= 1
  return( nimg( D ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Painting ----

#' Draw a circle to an image
#' @param im an image
#' @param cy circle center x
#' @param cx circle center y
#' @param r circle radius
#' @param color circle color
#' @return an image
#' @examples
#' im = paint_circle(regatta, 100, 200, 20, c(1, 0.2, 0.2))
#' plot(im)
#' @export
paint_circle = function(im, cy, cx, r, color){
  mask = get_circular_mask( im_height(im), im_width(im), cy, cx, r)
  mask = im_tricolored( mask )
  im[mask == 1] = rep( color, each = sum( mask ) / 3 )
  return( im )
}


#' Draw an arc to an image
#' @param im an image
#' @param cy arc center x
#' @param cx arc center y
#' @param r arc radius
#' @param w arc width
#' @param t1 angle (start) in radius
#' @param t2 angle (end) in radius
#' @param color circle color
#' @return an image
#' @examples
#' im = paint_arc(regatta, 200, 300, 100, 10, 0, pi, c(1, 0.2, 0.2))
#' plot(im)
#' @export
paint_arc = function(im, cy, cx, r, w, t1, t2, color){
  outer = get_circular_mask( im_height(im), im_width(im), cy, cx, r)
  inner = get_circular_mask( im_height(im), im_width(im), cy, cx, r - w)
  arc = get_arc_mask( im_height(im), im_width(im), cy, cx, r, t1, t2)
  outer[inner == 1] = 0
  outer[arc==0] = 0
  mask = im_tricolored( nimg( outer ) )
  im[ mask == 1 ] = rep( color, each = sum( mask ) / 3 )
  return(im)
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Spatial filtering ----


#' Edge detection by finite differences
#' @param im an image
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return a list containing images
#' @examples
#' edge = edge_diff(im_gray(regatta))
#' plot(edge$magnitude)
#' @export
edge_diff = function( im, pad.method = "mirror" ){
  fx = matrix( c( -1, 0, 1 ), ncol = 3 ) %>% nimg
  fy = matrix( c( -1, 0, 1 ), ncol = 1 ) %>% nimg
  gx = im_conv( im, fx, pad.method )
  gy = im_conv( im, fy, pad.method )
  magnitude = sqrt( gx^2 + gy^2 )
  theta = atan2( gy, gx )
  return( list( magnitude = magnitude, theta = theta, gx = gx, gy = gy ) )
}


#' Sobel edge detection
#' @param im an image
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return a list containing images
#' @examples
#' edge = edge_sobel(im_gray(regatta))
#' plot(edge$magnitude)
#' plot(1 - edge$magnitude, rescale = TRUE)
#' @export
edge_sobel = function( im, pad.method = "mirror" ){
  fx = matrix( c( 1, 2, 1, 0, 0, 0, -1, -2, -1 ) / 4, ncol = 3 ) %>% nimg
  fy = t( fx[,,] ) %>% nimg
  gx = im_conv( im, fx, pad.method )
  gy = im_conv( im, fy, pad.method )
  magnitude = sqrt( gx^2 + gy^2 )
  theta = atan2( gy, gx )
  return( list( magnitude = magnitude, theta = theta, gx = gx, gy = gy ) )
}


#' Prewitt edge detection
#' @param im an image
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return a list containing images
#' @examples
#' edge = edge_prewitt(im_gray(regatta))
#' plot(edge$magnitude)
#' plot(1 - edge$magnitude, rescale = TRUE)
#' @export
edge_prewitt = function( im, pad.method = "mirror" ){
  fx = matrix( c( 1, 1, 1, 0, 0, 0, -1, -1, -1 ) / 3, ncol = 3 ) %>% nimg
  fy = t( fx[,,] ) %>% nimg
  gx = im_conv( im, fx, pad.method )
  gy = im_conv( im, fy, pad.method )
  magnitude = sqrt( gx^2 + gy^2 )
  theta = atan2( gy, gx )
  return( list( magnitude = magnitude, theta = theta, gx = gx, gy = gy ) )
}


#' Edge detection by variance method
#' @param im an image
#' @param radius kernel radius
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return an image
#' @examples
#' edge = edge_variance(im_gray(regatta), radius = 1)
#' plot(edge, rescale = TRUE)
#' @export
edge_variance = function( im, radius, pad.method = "mirror" ){
  stat_filter( im, radius, stats::var, pad.method )
}


#' Edge detection by Max-Min method
#' @param im an image
#' @param radius kernel radius
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return an image
#' @examples
#' edge = edge_maxmin(im_gray(regatta), radius = 1)
#' plot(edge, rescale = TRUE)
#' @export
edge_maxmin = function( im, radius, pad.method = "mirror" ){
  stat_filter( im, radius, MaxMin, pad.method )
}


#' Apply the Canny edge detection
#'
#' This is the wrapper of the cannyEdges() function in the imager package.
#' t1 and t2 determines the edge threshold.
#' if the threshold parameters are missing, they are determined automatically using a k-means heuristic.
#' alpha parameter adjusts the automatic thresholds up or down.
#' The edge detection is based on a smoothed image gradient (set by the sigma parameter).
#' If smoothing parameter is set, high frequency noise is removed before applying the Canny edge detection.
#'
#' @param im an image.
#' @param t1 threshold for weak edges (if missing, both thresholds are determined automatically).
#' @param t2 threshold for strong edges.
#' @param alpha threshold adjustment factor (default 1).
#' @param sigma degree of smoothing image gradient.
#' @param smoothing numeric (integer). Smoothness of image texture.
#' @return an array of the edge image.
#' @examples
#' im = edge_canny(regatta)
#' plot(im)
#' @export
edge_canny = function( im, t1, t2, alpha = 1, sigma = 2, smoothing = 0 ){
  im = im_gray( im )
  N = floor( log2( min( im_size( im ) ) ) )
  if( smoothing > N ){
    warning( paste0( "smoothing exceeded the maximum possible value for this image. smoothing = ",
                     N, " was used instead.") )
    smoothing = N
  }
  if( smoothing >= 1 ){
    # im = gf_get_residual( im, smoothing )
  }
  im2 = imager::cannyEdges( nimg2cimg( im ), t1, t2, alpha, sigma )
  im2 = cimg2nimg( im2 )
  return( im2 )
}


# edge_DOG(im_gray(regatta), 0.5) %>% plot( rescale = T)
edge_DOG = function( im, sigma, k = 1.6 ){
  im_conv( im, gauss_kernel( sigma ) ) - im_conv( im, gauss_kernel( k * sigma ) )
}


# edge_XDOG(im_gray(regatta), 0.5) %>% ramp_threshold( 0.5, 6 ) %>% plot()
edge_XDOG = function( im, sigma, k = 1.6, p = 20 ){
  ( 1 + p ) * im_conv( im, gauss_kernel(sigma) ) - p * im_conv( im, gauss_kernel(k * sigma) )
}


#' Box blur
#' @param im an image
#' @param radius radius
#' @return an image
#' @examples
#' plot(box_blur(regatta, 10))
#' @export
box_blur = function( im, radius ){
  if( radius < 1 ){
    #warning( "radius should be equal to or larger than 1.")
    return( im )
  }
  r = radius
  if( im_nc( im ) != 1 ){
    imlist = list()
    for( i in 1:im_nc( im ) ){
      imlist = c( imlist, list( box_blur( get_channel( im, i ), r ) ) )
    }
    return( merge_color( imlist ) )
  }
  L = 2 * r + 1
  width = im_width( im )
  height = im_height( im )
  im = im_pad( im, r, method = "mirror" )
  out = array( 0.0, dim( im ) )
  cumsum = rowSums( im[ , 1:(2*r), ] )
  # i = r + 1
  cumsum = cumsum + im[ ,r + 1 + r, ]
  out[ , r + 1, ] = cumsum / L
  for( i in ( r + 2 ):( width + r ) ){
    cumsum = cumsum + im[ ,i + r, ] - im[ ,i - r - 1, ]
    out[ , i, ] = cumsum / L
  }
  im = out
  cumsum = colSums( im[ 1:(2*r), , ] )
  cumsum = cumsum + im[ r + 1 + r, , ]
  out[ r + 1, , ] = cumsum / L
  for( i in ( r + 2 ):( height + r ) ){
    cumsum = cumsum + im[ i + r, , ] - im[ i - r - 1, , ]
    out[ i, , ] = cumsum / L
  }
  out = im_crop( out, r )
  return( out )
}


#' Box variance
#' @param im an image
#' @param radius radius
#' @return an image
#' @examples
#' plot(box_variance(regatta, 3), rescale = TRUE)
#' @export
box_variance = function( im, radius ){
  box_blur( im^2, radius ) - box_blur( im, radius )^2
}


#' Create a Gaussian kernel
#' @param sd sd of the normal distribution
#' @param radius kernel radius. kernel diameter is 2 * radius + 1.
#' @return an image
#' @examples
#' plot(gauss_kernel(10), rescale = TRUE)
#' plot(gauss_kernel(sd = 10, radius = 20), rescale = TRUE)
#' @export
gauss_kernel = function( sd, radius = round( 2.5 * sd ) ){
  if( sd < 0.2 ){
    warning( "sd must be equal to or larger than 0.2")
    return( NULL )
  }
  L = 2 * radius + 1
  matx = matrix( stats::dnorm( 1:L, mean = radius + 1, sd = sd ), nrow = L, ncol = L, byrow = FALSE )
  maty = matrix( stats::dnorm( 1:L, mean = radius + 1, sd = sd ), nrow = L, ncol = L, byrow = TRUE )
  mat = matx * maty
  mat = mat / sum( mat )
  return( nimg( array( mat, c( L, L, 1 ) ) ) )
}


#' Create a gabor filter kernel
#' @param ksize the size of the gabor kernel. should be odd number (if not, incremented by one).
#' @param sigma the standard deviation of the Gaussian function
#' @param lambd the wavelength of the sinusoidal factor
#' @param theta the orientation of the normal to the parallel stripes of the Gabor function
#' @param psi the phase offset
#' @param gamma the spatial aspect ratio
#' @param normalize if TRUE (default), kernel is normalized (the zero-summing normalization)
#' @param mask if TRUE, circular mask is applied.
#' @examples
#' gb = gabor_kernel( ksize = 61 )
#' plot(gb, rescale = TRUE)
#' gb = gabor_kernel( ksize = 61, theta = pi/6 )
#' plot(gb, rescale = TRUE)
#' @export
gabor_kernel = function( ksize = sigma * 6, sigma = min( ksize ) / 6, lambd = min( ksize ) / 4,
                         theta = 0, psi = 0, gamma = 1, normalize = TRUE, mask = FALSE ){
  if( ksize %% 2 == 0 ){
    ksize = ksize + 1
  }
  if( length( ksize ) == 1 ){
    ksize = c( ksize, ksize )
  }
  sigmaX = sigma
  sigmaY = sigma / gamma
  nstds = 3
  c = cos( theta )
  s = sin( theta )
  xmax = ifelse( ksize[ 1 ] > 0, floor( ksize[ 1 ] / 2 ),
                 round( max( abs( nstds * sigmaX * c ), abs( nstds * sigmaY * s ) ) ) )
  ymax = ifelse( ksize[ 2 ] > 0, floor( ksize[ 2 ] / 2 ),
                 round( max( abs( nstds * sigmaX * s ), abs( nstds * sigmaY * c ) ) ) )
  xmin = -xmax
  ymin = -ymax
  scale = 1
  ex = -0.5 / ( sigmaX * sigmaX )
  ey = -0.5 / ( sigmaY * sigmaY )
  cscale = pi * 2 / lambd

  ix = matrix( xmin:xmax, nrow = ymax - ymin + 1, ncol = xmax - xmin + 1, byrow = TRUE )
  iy = matrix( ymin:ymax, nrow = ymax - ymin + 1, ncol = xmax - xmin + 1 )
  xr =  ix * c + iy * s
  yr = -ix * s + iy * c

  kernel = scale * exp( ex * xr * xr + ey * yr * yr ) * cos( cscale * xr - psi )

  if( mask ){
    size = max( ksize )
    cx = cy = ceiling( size / 2 )
    u = matrix( 1:size - cx, ncol = size, nrow = size, byrow = TRUE )
    v = matrix( 1:size - cy, ncol = size, nrow = size, byrow = FALSE )
    D = u^2 + v^2
    kernel[ D > (size/2)^2 ] = 0
  }

  if ( normalize ){
    psum = sum( kernel[ kernel > 0 ] )
    kernel[ kernel > 0 ] = kernel[ kernel > 0 ] / psum
    nsum = sum( kernel[ kernel < 0 ] )
    kernel[ kernel < 0 ] = kernel[ kernel < 0 ] / abs( nsum )
  }

  return( nimg( kernel ) )
}


#' Apply the guided filter
#' @param p an image
#' @param radius filter radius
#' @param epsilon epsilon parameter
#' @param I guide image
#' @return an image
#' @examples
#' plot(guided_filter(regatta,8))
#' @export
guided_filter = function( p, radius, epsilon = 0.1, I = p ){
  if( radius < 1 ){
    warning( "radius should be equal to or larger than 1.")
    return( p )
  }

  I_mean = box_blur( I, radius )
  I_var = box_variance( I, radius )
  p_mean = box_blur( p, radius )

  a = ( box_blur( I * p, radius ) - I_mean * p_mean ) / ( I_var + epsilon )
  b = p_mean - a * I_mean

  a_mean = box_blur( a, radius )
  b_mean = box_blur( b, radius )

  q = a_mean * I + b_mean
  return( q )
}


#' Apply statistical filter
#' @param im an image
#' @param radius kernel radius
#' @param FUN e.g., min, max, median, mean, var
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return an image
#' @examples
#' plot(stat_filter(regatta, 1, min))
#' @export
stat_filter = function( im, radius, FUN, pad.method = "mirror" ){
  if( radius < 1 ){
    warning( "radius should be equal to or larger than 1.")
    return( im )
  }

  if( im_nc( im ) > 1 ){
    imlist = list()
    for( i in 1:im_nc( im ) ){
      imlist = c( imlist, list( stat_filter( get_channel( im, i ), radius, FUN, pad.method ) ) )
    }
    return( merge_color( imlist ) )
  }

  im = im_pad( im, radius, method = pad.method )[,,]
  im2 = im
  for( cy in ( 1 + radius ):( im_height( im ) - radius ) ){
    for( cx in ( 1 + radius ):( im_width( im ) - radius ) ){
      im2[ cy, cx ] = FUN(
        as.vector( im[ ( cy - radius ):( cy + radius ), ( cx - radius ):( cx + radius ) ] )
      )
    }
  }
  im2 = im_crop( nimg( im2 ), radius )
  return( im2 )

  # im = im_pad( im, radius, method = pad.method )[,,]
  # im2 = matrix2list( im )
  # for( cy in ( 1 + radius ):( im_height( im ) - radius ) ){
  #   for( cx in ( 1 + radius ):( im_width( im ) - radius ) ){
  #     im2[[ cx ]][ cy ] = FUN(
  #       as.vector( im[ ( cy - radius ):( cy + radius ), ( cx - radius ):( cx + radius ) ] )
  #     )
  #   }
  # }
  # im2 = list2matrix( im2 )
  # im2 = im_crop( nimg( im2 ), radius )
  # return( im2 )
}


#' Convolve an image
#' @param im an image
#' @param kernel filter image
#' @param pad.method either "zero", "mean", "repeat", "mirror", or a numeric value
#' @return an image
#' @examples
#' plot(im_conv(regatta, gauss_kernel(sd = 2)))
#' @export
im_conv = function( im, kernel, pad.method = "mirror" ){
  if( is.null( kernel ) ){
    return( im )
  }
  if( im_nc( im ) > 1 ){
    imlist = list()
    for( i in 1:im_nc( im ) ){
      imlist = c( imlist, list( im_conv( get_channel( im, i ), kernel, pad.method ) ) )
    }
    return( merge_color( imlist ) )
  }
  npad = floor( max( dim( kernel )[ 1:2 ] ) / 2 )
  im = im_pad( im, n = npad, method = pad.method )
  im = imager::convolve( nimg2cimg( im ), nimg2cimg( kernel ) )
  im = imager::crop.borders( im, nPix = npad )
  return( cimg2nimg( im ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# V1 model ----


#' Get V1 filter parameters
#' @return a data frame
#' @export
V1_params = function(){
  df = data.frame(
    band = rep( 1:8, each = 2 ),
    scale = rep( 1:2, times = 8 ),
    ksize = seq( from = 7, by = 2, to = 37 ),
    sigma = c( 2.8,3.6,4.5,5.4,6.3,7.3,8.2,9.2,10.2,11.3,12.3,13.4,14.6,15.8,17.0,18.2 ),
    lambd = c( 3.5,4.6,5.6,6.8,7.9,9.1,10.3,11.5,12.7,14.1,15.4,16.8,18.2,19.7,21.2,22.8 ),
    gamma = 0.3
  )
  return( df )
}


#' Create a Gabor filter bank
#' @param id a vector of integer. which cell to use.
#' @param n_orientation number of orientation
#' @param cell_type either "simple" or "complex" (default) cell
#' @return a list of filter kernels
#' @export
v1_kernels = function( id, n_orientation = 4, cell_type = "complex" ){
  filters = list()
  pars = V1_params()
  pars = pars[ id, ]
  for( i in 1:nrow( pars ) ){
    p = pars[ i, ]
    n_ori = ifelse( n_orientation == 0, 1, n_orientation )
    for( t in 1:n_ori ){
      name = paste0( "i", length( filters ) + 1, "_g", i, "_ori", t )
      lambd_ = ifelse( n_orientation == 0, 1, p$lambd )
      gamma_ = ifelse( n_orientation == 0, 1, p$gamma )
      theta_ = ifelse( n_orientation == 0, 0, ( t - 1 ) * pi / n_ori )
      f = gabor_kernel( ksize = p$ksize, sigma = p$sigma, lambd = lambd_,
                        gamma = gamma_, theta = theta_ )
      filters = c( filters, list( name = f ) )
      names( filters )[ length( filters ) ] = name
      if( cell_type == "complex" ){
        name = paste0( "i", length( filters ) + 1, "_g", i, "_ori", t, "_sine" )
        f = gabor_kernel( ksize = p$ksize, sigma = p$sigma, lambd = lambd_, psi = pi / 4,
                          gamma = gamma_, theta = theta_ )
        filters = c( filters, list( name = f ) )
        names( filters )[ length( filters ) ] = name
      }
    }
  }
  return( filters )
}


#' Apply V1 filters to an image
#' @param im an image
#' @param id a vector of integer. which cell to use.
#' @param n_orientation number of orientation
#' @param cell_type either "simple" or "complex" (default) cell
#' @return a list of images
#' @examples
#' im = filter_v1(im_gray(regatta), id = 1:2, n_orientation = 4, cell_type = "complex")
#' plot(im[[1]], rescale = TRUE)
#' @export
filter_v1 = function( im, id, n_orientation = 4, cell_type = "complex" ){
  output = list()
  filt = v1_kernels( id, n_orientation, cell_type )

  if( cell_type == "simple" ){
    for( i in 1:length( filt ) ){
      output = c( output, list( im_conv( im, filt[[ i ]] ) ) )
    }
  } else if( cell_type == "complex" ){
    for( i in seq( 1, length( filt ), by = 2 ) ){
      im1 = im_conv( im, filt[[ i + 0 ]] )
      im2 = im_conv( im, filt[[ i + 1 ]] )
      output = c( output, list( sqrt( im1^2 + im2^2 ) ) )
    }
  }

  return( output)
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# FFT ----

#' Blur an image
#' @param im an image
#' @param cutoff cutoff frequency
#' @param method either "gauss", "ideal", or "butterworth"
#' @param order order of butterworth filter
#' @examples
#' plot(im_blur(regatta, 20))
#' @export
im_blur = function( im, cutoff, method = "gauss", order = 4 ){
  n = round( min( im_size( im ) ) / 10 )
  im = im_pad( im, n = n, method = "mirror" )
  im2 = fft_filter( im, xpass = "lowpass", kernel = method, cutoff = cutoff, order = order )
  im2 = im_crop( im2, n )
  im2 = clamping( im2 )
  return( im2 )
}


#' Shift fft image
#' @param im an image
#' @param inverse inverse transform
#' @return an image
#' @export
fft_shift = function( im, inverse = FALSE ){
  lagx = floor( im_width( im ) / 2 )
  lagy = floor( im_height( im ) / 2 )
  if( inverse ){
    lagx = -lagx
    lagy = -lagy
  }
  im = im_shift( im, "x", lagx )
  im = im_shift( im, "y", lagy )
  return( im )
}


#' forward fft
#' @param im an image
#' @return an image
#' @export
for_fft = function( im ){
  fft_shift( stats::fft( im ) / length( im ) )
}


#' inverse fft
#' @param im an image
#' @return an image
#' @export
inv_fft = function( im ){
  Re( stats::fft( fft_shift( im, inverse = TRUE ), inverse = TRUE ) )
}


#' Get lowpass kernel
#' @param kernel either "ideal", "gauss", "butterworth", or "dc"
#' @param height image height
#' @param width image width
#' @param cutoff cutoff frequency
#' @param order order of butterworth filter
#' @return an image
#' @export
fft_lowpass_kernel = function( kernel, height, width, cutoff, order = 4 ){
  cx = floor( width / 2 ) + 1
  cy = floor( height / 2 ) + 1
  u = matrix( 1:width - cx, ncol = width, nrow = height, byrow = T )
  v = matrix( 1:height - cy, ncol = width, nrow = height, byrow = F )
  D = sqrt( u^2 + v^2 )
  if( kernel == "ideal" ){
    H = D
    H[ D <= cutoff ] = 1
    H[ D > cutoff ] = 0
  } else if( kernel == "gauss" ){
    H = exp( -1 * D * D / 2 / cutoff / cutoff )
  } else if( kernel %in% c( "butterworth", "bw", "BW", "btw" ) ){
    H = 1 / ( 1 + ( D / cutoff )^( 2 * order ) )
  } else if( kernel  %in% c( "dc", "DC" ) ){
    H = matrix( 0, ncol = width, nrow = height )
    H[ cy, cx ] = 1
  }
  return( H )
}


#' Apply fft filter
#' @param im an image
#' @param xpass either "lowpass", "highpass", or "bandpass"
#' @param kernel either "ideal", "gauss", "butterworth", or "dc"
#' @param cutoff cutoff frequency
#' @param cutoff2 second cutoff frequency
#' @param order order of butterworth filter
#' @return an image
#' @examples
#' plot(fft_filter(regatta, "lowpass", "ideal", 10)) # ideal lowpass filterig
#' plot(fft_filter(regatta, "lowpass", "gauss", 10)) # gaussian lowpass filterig
#' # 5th-order butterworth lowpass filtering
#' plot(fft_filter(regatta, "lowpass", "butterworth", 10, order = 5))
#' im = im_gray(regatta)
#' plot(fft_filter(im, "highpass", "gauss", 60), rescale = TRUE) # gaussian highpass filterig
#' plot(fft_filter(im, "bandpass", "gauss", 10, 20), rescale = TRUE) # gaussian bandpass filterig
#' @export
fft_filter = function( im, xpass, kernel = "ideal", cutoff, cutoff2, order = 4 ){
  if( xpass == "lowpass" ){
    fft_mask = fft_lowpass_kernel( kernel, im_height( im ), im_width( im ), cutoff, order ) %>%
      im_rep( im_nc( im ) )
  } else if( xpass == "highpass" ){
    fft_mask = 1 - fft_lowpass_kernel( kernel, im_height( im ), im_width( im ), cutoff, order ) %>%
      im_rep( im_nc( im ) )
  } else if( xpass == "bandpass" ){
    low = fft_lowpass_kernel(
      kernel, im_height( im ), im_width( im ), max( cutoff, cutoff2 ), order ) %>%
      im_rep( im_nc( im ) )
    high = 1 - fft_lowpass_kernel(
      kernel, im_height( im ), im_width( im ), min( cutoff, cutoff2 ), order ) %>%
      im_rep( im_nc( im ) )
    fft_mask = pmin( low, high )
  }
  im2 = inv_fft( for_fft( im ) * fft_mask )
  return( im2 )
}


#' fft phase scrambling
#' @param im an image
#' @return an image
#' @examples
#' plot(fft_phase_scramble(regatta))
#' @export
fft_phase_scramble = function( im ){
  if( im_nc( im ) >= 3 ){
    ls = list()
    for( i in 1:im_nc( im ) ){
      ls = c( ls, list( fft_phase_scramble( get_channel( im, i ) ) ) )
    }
    out = merge_color( ls )
    return( out )
  }
  #
  imf = stats::fft( im ) / length( im )
  A = abs( imf )
  P = atan2( Im( imf ), Re( imf ) )
  #
  h = im_height( im )
  w = im_width( im )
  cy = floor( h / 2 ) + 1
  cx = floor( w / 2 ) + 1
  p = array( stats::runif( prod( im_size( im ) ), min = -pi, max = pi ), dim = im_size( im ) )
  p[ 1, 1 ] = 0
  if( im_height( im ) %% 2 == 1 & im_width( im ) %% 2 == 1 ){ # odd, odd
    p[ 2:cy, 1 ] = -p[ h:(cy+1), 1 ]
    p[ 1, 2:cx ] = -p[ 1, w:(cx+1) ]
    p[ 2:h, 2:cx ] = -array( rev( p[ 2:h, (cx+1):w ] ), dim = dim( p[ 2:h, 2:cx ] ) )
  } else if( im_height( im ) %% 2 == 1 & im_width( im ) %% 2 == 0 ){ # odd, even
    p[ 2:cy, 1 ] = -p[ h:(cy+1), 1 ]
    p[ 1, 2:(cx-1) ] = -p[ 1, w:(cx+1) ]
    p[ 2:h, 2:(cx-1) ] = -array( rev( p[ 2:h, (cx+1):w ] ), dim = dim( p[ 2:h, 2:(cx-1) ] ) )
  } else if( im_height( im ) %% 2 == 0 & im_width( im ) %% 2 == 1 ){ # even, odd
    p[ 2:(cy-1), 1 ] = -p[ h:(cy+1), 1 ]
    p[ 1, 2:cx ] = -p[ 1, w:(cx+1) ]
    p[ 2:(cy-1), 2:w ] = -array( rev( p[ (cy+1):h, 2:w ] ), dim = dim( p[ 2:(cy-1), 2:w ] ) )
  } else if( im_height( im ) %% 2 == 0 & im_width( im ) %% 2 == 0 ){ # even, even
    p[ 2:(cy-1), 1 ] = -p[ h:(cy+1), 1 ]
    p[ 1, 2:(cx-1) ] = -p[ 1, w:(cx+1) ]
    p[ 2:(cy-1), 2:(cx-1) ] = -array( rev( p[ (cy+1):h, (cx+1):w ] ),
                                      dim = dim( p[ 2:(cy-1), 2:(cx-1) ] ) )
    p[ (cy+1):h, 2:(cx-1) ] = -array( rev( p[ 2:(cy-1), (cx+1):w ] ),
                                      dim = dim( p[ (cy+1):h, 2:(cx-1) ] ) )
  }
  dim( p ) = c( dim( p ), 1 )
  im2 = Re( stats::fft( A * exp( 1i * p ), inverse = T ) ) %>% clamping
  # imf2 = complex( real = A * cos( p ), imaginary = A * sin( p ) )
  # im2 = Re( stats::fft( array( imf2, dim = c( im_size( im ), 1, 1 ) ), inverse = T ) )
  return( nimg( im2 ) )
}


#' Calculate fft amplitude
#' @param im an image
#' @param modify if TRUE, amplitude value is log-rescaled, which is useful for plotting purpose
#' @param dc_value if given, DC (direct current) component is replaced with this value
#' @return an image
#' @examples
#' amp = fft_amplitude(im_gray(regatta), modify = TRUE)
#' plot(amp)
#' @export
fft_amplitude = function( im, modify = FALSE, dc_value ){
  if( is.list( im ) ){
    for( i in 1:length( im ) ){
      if( base::missing( dc_value ) ){
        im[[ i ]] = fft_amplitude( im[[ i ]], modify )
      } else {
        im[[ i ]] = fft_amplitude( im[[ i ]], modify, dc_value )
      }
    }
    return( im )
  }
  imf = abs( for_fft( im ) )
  if( ! base::missing( dc_value ) ){
    cx = floor( im_width( im ) / 2 ) + 1
    cy = floor( im_height( im ) / 2 ) + 1
    imf[ cy, cx, ] = dc_value
  }
  if( modify ){
    return( imf %>% + max( .Machine$double.eps, min( . ) ) %>% log %>% rescaling01 %>% .^2 )
  } else {
    return( imf )
  }
}


#' Calculate fft phase
#' @param im an image
#' @return an image
#' @examples
#' phase = fft_phase(im_gray(regatta))
#' plot(phase, rescale = TRUE)
#' @export
fft_phase = function( im ){
  imf = for_fft( im )
  return( atan2( Im( imf ), Re( imf ) ) )
}


#' Calculate fft amplitude and phase
#' @param im an image
#' @param modify if TRUE, amplitude value is log-rescaled, which is useful for plotting purpose
#' @return an image
#' @examples
#' img = fft_spectrum(im_gray(regatta), modify = TRUE)
#' plot(img$magnitude)
#' plot(img$phase, rescale = TRUE)
#' @export
fft_spectrum = function( im, modify = FALSE ){
  if( is.list( im ) ){
    for( i in 1:length( im ) ){
      im[[ i ]] = fft_spectrum( im[[ i ]], modify )
    }
    return( im )
  }
  imf = for_fft( im )
  if( modify ){
    return( list(
      magnitude = abs( imf ) %>%
        + max( .Machine$double.eps, min( . ) ) %>% log %>% rescaling01 %>% .^2,
      phase = atan2( Im( imf ), Re( imf ) ) )
    )
  } else {
    return( list( magnitude = abs( imf ), phase = atan2( Im( imf ), Re( imf ) ) )
    )
  }
}


#' Create fft angular mask
#' @param height image height
#' @param width image width
#' @param orientation mask orientation in radians
#' @param angle angle of mask in radians
#' @param cutoff cutoff frequencies
#' @return an image
#' @export
fft_angular_mask = function( height, width, orientation = 0, angle = 0,
                             cutoff = c( 0, min( width, height ) / 2 ) ){
  orientation = mod( orientation, 2 * pi )
  angle = angle / 2

  cx = floor( width / 2 ) + 1
  cy = floor( height / 2 ) + 1

  u = matrix( 1:width - cx, ncol = width, nrow = height, byrow = T )
  v = matrix( 1:height - cy, ncol = width, nrow = height, byrow = F )
  D = sqrt( u^2 + v^2 )

  A = -atan2( v, u )
  A[ A < 0 ] = A[ A < 0 ] + 2 * pi

  H = matrix( 0, ncol = width, nrow = height )
  if( angle == 0 ){
    mask = A == orientation | A == pi
  } else if( orientation - angle < 0 || orientation + angle >= 2 * pi ){
    mask = A >= mod( orientation - angle, 2 * pi ) | A <= mod( orientation + angle, 2 * pi )
    orientation = orientation + pi
    mask = mask | ( A >= mod( orientation - angle, 2*pi ) & A <= mod( orientation + angle, 2 * pi ) )
  } else {
    mask = A >= mod( orientation - angle, 2 * pi ) & A <= mod( orientation + angle, 2 * pi )
    orientation = orientation + pi
    mask = mask | A >= mod( orientation - angle, 2 * pi ) & A <= mod( orientation + angle, 2 * pi )
  }
  H[ mask & !(D <= cutoff[ 1 ]) & D <= cutoff[ 2 ] ] = 1
  H[ cy, cx ] = 0 # exclude DC component

  return( nimg( H ) )

  # angular completeness
  circ = fft_angular_mask( 256, 256, 0, 2 * pi )
  n = 360 / 1
  ang = 2 * pi / n
  ori = seq( from = 0, by = ang, length.out = n )
  S = 0
  M = NULL
  for ( i in ori ) {
    mask = fft_angular_mask( 256, 256, i, ang )
    S = S + sum( mask )
    if( is.null( M ) ){
      M = mask
    } else {
      M = M | mask
    }
  }
  print(sum(circ)) # region of interest
  print(sum(M)) # covered region by the sum of each mask
  print(S/2) # can be smaller/larger than sum(circ)

  # distance completeness
  resol = 128
  circ = fft_angular_mask( resol, resol, 0, 2 * pi )
  step = 1
  dist = c( seq( 0, by = step, to = resol / 2 ) )
  S = 0
  M = NULL
  for ( i in 1:( resol / 2 / step ) ) {
    mask = fft_angular_mask( resol, resol, 0, 2 * pi, c( dist[ i ], dist[ i + 1 ] ) )
    S = S + sum( mask )
    if( is.null( M ) ){
      M = mask
    } else {
      M = M | mask
    }
  }
  print(sum(circ))
  print(sum(M))
  print(S) # must be (and is) equal to sum(circ)
}


#' Calculate orientation-averaged amplitude
#' @param im an image
#' @param range frequency range
#' @param n number of samples
#' @param mask if TRUE, apply circular mask before calculation
#' @param linear.sample if TRUE,
#' @param rawdata if TRUE, raw data is returned as well as slope value
#' @param show.plot if TRUE, figure is displayed
#' @return a numeric value or a data frame
#' @examples
#' fft_slope(regatta)
#' fft_slope(regatta, rawdata = TRUE, show.plot = TRUE)
#' @export
fft_slope = function( im, range = c(10, 512), n = 10, mask = TRUE, linear.sample = TRUE,
                      rawdata = FALSE, show.plot = FALSE ){
  im = im_crop_square( im_gray( im ) )
  height = im_height( im )
  width = im_width( im )

  # add oval mask to the input image
  if( mask ){
    filt = fft_lowpass_kernel( "bw", height, width, height * 0.47, 20 )
    filt = nimg( filt )
    im = filt * im + ( 1 - filt ) * mean( im )
  }

  # sample bins (distance from the origin) for rotational averaging
  nyquist = floor( min( im_size( im ) ) / 2 )
  if( linear.sample ){
    dist = logspace( range[ 1 ], min( range[ 2 ], nyquist ), n + 1 )
  } else {
    dist = seq( from = range[ 1 ], to = min( range[ 2 ], nyquist ), length.out = n + 1 )
  }

  # calculate rotational average amplitude
  amplitudes = vector( "numeric", n )
  imf = fft_amplitude( im ) * width * height
  for ( i in 1:( length( dist ) - 1 ) ) {
    maskim = fft_angular_mask( height, width, 0, 2 * pi, c( dist[ i ], dist[ i + 1 ] ) )
    amplitudes[ i ] = mean( imf[ maskim > 0 ] )
  }

  # lm fit
  dat = data.frame( cpp = dist[ 2:length( dist ) ], amplitude = amplitudes )
  dat = stats::na.omit( dat )
  df = data.frame( log_cpp = log10( dat$cpp ), log_amplitude = log10( dat$amplitude ) )
  fit = stats::lm( log_amplitude ~ log_cpp, data = df )
  slope = unname( fit$coefficients[ 2 ] )
  df$slope = as.numeric( sprintf( "%1.3f", slope ) )
  rmse = sqrt( mean( fit$residuals^2 ) ) %>% sprintf( "%1.4f", . ) %>% as.numeric
  df$RMSE = rmse

  vmax = ceiling( max( c( df$log_cpp, df$log_amplitude ) ) ) %>% max( c( ., 4) )
  if( show.plot ){
    fig = ggplot( df, aes_string( x = "log_cpp", y = "log_amplitude" ) ) +
      geom_point() +
      geom_smooth( method = "lm", formula = y ~ x, se = FALSE ) +
      annotate( "text", x = 1, y = 1.2, label = sprintf("Slope = %1.3f", slope), size = 8 ) +
      annotate( "text", x = 1, y = 0.8, label = sprintf("RMES = %1.4f", rmse),  size = 8 ) +
      scale_x_continuous( limits = c( 0, vmax ), breaks = 0:vmax, expand = c( 0, 0 ) ) +
      scale_y_continuous( limits = c( 0, vmax ), breaks = 0:vmax, expand = c( 0, 0 ) )
    plot( fig )
  }

  if( rawdata ){
    return( df )
  } else {
    return( slope )
  }
}


#' Generate a white noise image
#' @param height image height
#' @param width image width
#' @return an image
#' @examples
#' plot(fft_noise(128, 128))
#' @export
fft_noise = function( height, width ){
  # mean(im^2) = sum(A^2)
  # replace all A values with sqrt( mean(a^2) )
  tgt = 0.5
  A = array( sqrt( tgt^2 / ( height * width ) ), dim = c( height, width, 1 ) )
  p = array( stats::runif( height * width, min = -pi, max = pi ), dim = c( height, width, 1 ) )

  im2 = Re( stats::fft( A * exp( 1i * p ), inverse = T ) ) %>% rescaling01 %>% nimg
  # plot(im2)
  # fft_slope( im2 )
  return( im2 )
}


fft_transfer = function( from, to, element ){
  if( element %in% c( "amplitude", "magnitude" ) ){
    A = fft_amplitude( from )
    P = fft_phase( to )
    im = inv_fft( A * exp( 1i * P ) )  %>% clamping
  } else if( element == "phase" ){
    A = fft_amplitude( to )
    P = fft_phase( from )
    im = inv_fft( A * exp( 1i * P ) )  %>% clamping
  }
  return( im )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Image statistics ----

#' Mean squared error between two images
#' @param im1 an image
#' @param im2 an image
#' @return mean squared error
#' @examples
#' im_diff(regatta, regatta^2)
#' @export
im_diff = function( im1, im2 ){
  if( imager::is.cimg( im1 ) ){
    im1 = cimg2nimg( im1 )
  }
  if( imager::is.cimg( im2 ) ){
    im2 = cimg2nimg( im2 )
  }
  return( mean( ( im1 - im2 )^2 ) )
}


#' Get moment statistics of a vector/array
#' @param x data
#' @param order order of the moment to be computed
#' @param na.rm logical. Should missing values be removed?
#' @return a numeric vector
#' @examples
#' get_moments( rnorm( 20 ) ) # get the 1st to 4th order moments
#' get_moments( rnorm( 20 ), order = 3 ) # get only the 3rd order moment (skewness)
#' get_moments( rnorm( 20 ), order = c( 3, 1 ) ) # get skewness (3rd moment) and mean (1st moment)
#' @export
get_moments = function( x, order = 1:4, na.rm = FALSE ){
  m = rep( 0.0, times = length( order ) )
  names( m ) = c( "mean", "sd", "skewness", "kurtosis" )[ order ]
  for( i in 1:length( order ) ){
    if( order[ i ] == 1 ){
      m[ i ] = base::mean( x, na.rm = na.rm )
    } else if( order[ i ] == 2 ){
      m[ i ] = stats::sd( x, na.rm = na.rm )
    } else if( order[ i ] == 3 ){
      m[ i ] = moments::skewness( x, na.rm = na.rm )
    } else if( order[ i ] == 4 ){
      m[ i ] = moments::kurtosis( x, na.rm = na.rm )
    }
  }
  return( m )
}


#' Get moment statistics of an image
#' @param im an image
#' @param channel color channel
#' @param order order of the moment to be computed
#' @param space color space, either "CIELAB" (default) or "RGB"
#' @param max_size resize input image before calculation of moments
#' @param na.rm logical. Should missing values be removed?
#' @return a data frame of moment values
#' @examples
#' im_moments(regatta) # moments in CIELAB color space
#' im_moments(regatta, space = "RGB") # moments of RGB channels
#' im_moments(regatta, channel = 1) # L channel of CIELAB color space
#' im_moments(regatta, channel = "L") # same as above
#' im_moments(regatta, channel = 1, space = "RGB") # R channel of the input image
#' im_moments(regatta, channel = 2:3, order = c(2, 3)) # sd and skew in a and b channels
#' im_moments(regatta, channel = c("a", "b"), order = c(2, 3)) # same as above
#' @export
im_moments = function( im, channel = 1:3, order = 1:4, space = "CIELAB", max_size = 1024, na.rm = FALSE ){
  if( im_nc( im ) == 1 ){
    channel = 1
  }
  df = data.frame()
  im = im_resize_limit( im, max_size )
  if( space == "CIELAB" ){
    if( im_nc( im ) > 2 ){
      im = sRGB2Lab( im )
    }
    clabel = c( "L", "a", "b" )
  } else {
    clabel = c( "R", "G", "B", "A" )
  }
  channel = force_channel_label_to_num( channel )
  for( i in 1:length( channel ) ){
    mmt = get_moments( get_channel( im, channel[ i ] ), order, na.rm = na.rm )
    df = rbind( df, data.frame(
      channel = clabel[ channel[ i ] ], moment = names( mmt ), value = unname( mmt ) ) )
  }
  return( df )
}


#' Shift and scale the distribution of pixel values
#' @param im an image
#' @param channel color channel
#' @param mean center of distribution. when not given, the mean of that channel is used.
#' @param sd dispersion of distribution. when not given, the sd of that channel is used.
#' @param space color space
#' @param clamp either TRUE (default, output pixel value is clamped to range 0-1) or FALSE
#' @return an image
#' @examples
#' im_moments(regatta) # before manipulation
#' im_moments(im_distribute(regatta, "b", mean = 0, sd = 20)) # b channel is adjusted
#' plot(im_distribute(regatta, "b", mean = 0, sd = 20)) # see the effect
#' plot(im_distribute(regatta, c("a", "b"), c(-5, 0), c(15, 20))) # adjust two channels simultaneously
#' @export
im_distribute = function( im, channel, mean = NULL, sd = NULL, space = "CIELAB", clamp = TRUE ){
  channel = force_channel_label_to_num( channel )
  if( space == "CIELAB" && im_nc( im ) > 2 ){
    im = sRGB2Lab( im )
  }
  for( i in 1:length( channel ) ){
    if( is.null( mean[ i ] ) || is.na( mean[ i ] ) ){
      M = base::mean( get_channel( im, channel[ i ] ) )
    } else {
      M = mean[ i ]
    }
    if( is.null( sd[ i ] ) || is.na( sd[ i ] ) ){
      S = stats::sd( get_channel( im, channel[ i ] ) )
    } else {
      S = sd[ i ]
    }
    I = im[ , , channel[ i ], drop = F ]
    im[ , , channel[ i ] ] = S * ( ( I - base::mean( I ) ) / stats::sd( I ) ) + M
  }
  if( space == "CIELAB" && im_nc( im ) > 2 ){
    im = Lab2sRGB( im )
  }
  if( clamp ){
    im = clamping( im )
  }
  return( im )
}


#' Analyze edge distribution
#' @param im an image
#' @param bin.width bin width in degree
#' @param is.orientation TRUE for orientation, FALSE for direction
#' @examples
#' df = angle_distribution(regatta)
#' plot(df$Angle, df$Magnitude, type = "h")
#' @export
angle_distribution = function( im, bin.width = 3, is.orientation = TRUE ){
  G = get_gradient( get_L( im ), starts.horizontal = T )
  angle = round( ( as.vector( G$theta ) + pi ) * 180 / pi ) %>% clamping( 0, 360 )
  angle[ angle == 360 ] = 0
  angle = bin.width * ( angle %/% bin.width )
  if( is.orientation ){
    angle[ angle >= 180 ] = angle[ angle >= 180 ] - 180
  }

  df = data.frame( Angle = angle, magnitude = as.vector( G$magnitude ) ) %>%
    dplyr::group_by( .data$Angle ) %>%
    dplyr::summarise( Magnitude = sum( .data$magnitude ) ) %>%
    dplyr::mutate( Normalized = .data$Magnitude / sum( .data$Magnitude ) )
  return( as.data.frame(df) )

  # im = regatta
  # bin.width = 3
  # is.orientation = TRUE
  #
  # df = angle_distribution( im, bin.width, is.orientation )
  #
  # fig = ggplot( df, aes( x = Angle, y = Normalized ) ) +
  #   geom_bar( stat = "identity", width = bin.width ) +
  #   scale_x_continuous( limits = c( -bin.width / 2, ifelse( is.orientation, 180, 360 ) ), breaks = 0:120 * 30 )
  #
  # ggplot( as.data.frame( im ), aes( x, y ) ) +
  #   geom_raster( aes( fill = color ) ) +
  #   scale_fill_identity() +
  #   coord_fixed() +
  #   scale_y_continuous( trans = "reverse", expand = c( 0, 0 ) ) +
  #   scale_x_continuous( expand = c( 0, 0 ) ) +
  #   theme(
  #     axis.title = element_blank(),
  #     axis.text = element_blank(),
  #     axis.ticks = element_blank(),
  #     axis.line = element_blank(),
  #     plot.margin= unit(c(0, 0, 0, 0), "lines")
  #   ) -> fig_im
  #
  # fig = patchwork::wrap_plots( list( fig_im, fig ), nrow = 1 )
  # plot( fig )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Color Statistics ----

#' Circular color statistics
#' @param im an image
#' @examples
#' df = circular_color_stat(regatta)
#' @export
circular_color_stat = function( im ){
  # RGB to IHLS
  R = as.vector( get_R( im ) )
  G = as.vector( get_G( im ) )
  B = as.vector( get_B( im ) )
  M = matrix( c( 0.2125, 0.7154, 0.0721, 1, -1/2, -1/2, 0, -sqrt(3)/2, sqrt(3)/2 ), ncol = 3, byrow = T )
  M %*% c( R[1], G[1], B[1] )
  X = M %*% matrix( c( R, G, B ), nrow = 3, byrow = T )
  Y  = X[1, ]
  C1 = X[2, ]
  C2 = X[3, ]
  C = sqrt( C1^2 + C2^2 )
  H = rep( NA, length( C ) )
  H[ C == 0 ] = NA
  H[ C != 0 & C2 <= 0 ] = acos( C1[ C != 0 & C2 <= 0 ] / C[ C != 0 & C2 <= 0 ] )
  H[ C != 0 & C2 >  0 ] = 2 * pi - acos( C1[ C != 0 & C2 > 0 ] / C[ C != 0 & C2 > 0 ] ) # C2 > 0 is equivalent to B > G
  Hs = H %% ( pi / 3 )
  S = 2 * C * sin( pi * 2 / 3 - Hs ) / sqrt( 3 )

  # un-weighted
  A = sum( cos( H ), na.rm = TRUE )
  B = sum( sin( H ), na.rm = TRUE )
  Hue = atan2( B, A )
  R = sqrt( A^2 + B^2 )
  n = sum( !is.na( H ) )
  MeanLength = R / n
  Variance = 1 - MeanLength

  # weighted
  As = sum( S[ ! is.na( S ) ] * cos( H[ ! is.na( H ) ] ) )
  Bs = sum( S[ ! is.na( S ) ] * sin( H[ ! is.na( H ) ] ) )
  w_Hue = atan2( Bs, As )
  w_MeanLength_1 = sqrt( As^2 + Bs^2 ) / sum( S[ ! is.na( S ) ] )
  w_MeanLength_2 = sqrt( As^2 + Bs^2 ) / n
  w_Variance_1 = 1 - w_MeanLength_1
  w_Variance_2 = 1 - w_MeanLength_2

  df = data.frame(
    Hue, Variance, w_Hue, w_Variance_1, w_Variance_2
  )
  return( df )
}


#' Extract the color palette of an image
#' This function performs color palette extraction of an image using the mean shift algorithm.
#' It uses the meanShift function in the meanShiftR package.
#' @param im an image
#' @param bandwidth This value is used in the kernel density estimate for color classification.
#' The smaller the value of this parameter, the more colors are extracted.
#' @param bound Input image is resized before color classification.
#' The input image will be reduced in size, with the shorter side equal to this value.
#' @param iterations The number of iterations to perform mean shift classification.
#' @examples
#' palette = palette_extract(regatta)
#' palette$df
#' plot(palette$im)
#' @export
palette_extract = function( im, bandwidth = 0.03, bound = 40, iterations = 1000 ){
  height = im_height(im)
  width = im_width(im)

  im = im_resize_limit_min( im, bound )
  im2 = array( im_serialize( im ), c( im_width(im) * im_height(im), 3 ) )
  ms = meanShiftR::meanShift(
    im2, im2, bandwidth = rep( bandwidth, 3), iterations = iterations
  )
  col = unique( ms$value )
  col = sprintf( "%0.4f", col ) %>% as.numeric %>% matrix( ncol = 3 )
  df = data.frame( col )
  names( df ) = c( "R", "G", "B" )
  df$n = table( ms$assignment )
  df$percent = smart.round( df$n/sum(df$n), digits = 3 ) * 100
  df = df[ order( df$n, decreasing = TRUE ), ]
  rownames( df ) = NULL

  p = im_serialize_inv(as.vector(ms$value), im_height(im), im_width(im))
  p2 = im_resize(p, height, width, interpolation = 0)

  return( list( df = df, im = p2) )
}


#' Visualize the color palette of an image
#' @param im an image
#' @param palette color palette
#' @param type either "bar" or "circle
#' @param threshold minor colors below this threshold is not displayed
#' @param monospace either TRUE or FALSE. If FALSE, If FALSE, the length of the color chart is
#' proportional to the percentage of occurrences of that color in the image.
#' @param width width of color chart. A numeric value between 0 and 1.
#' @param size output image resolution. Only works for the circle type. The default is 1024.
#' @param outline_width outline width of color chart. The default is 0 (no outline).
#' @param outline_color outline color.
#' @param margin margin between picture and color chart. Only works for the bar type.
#' A numeric value between 0 and 1. The default is 0.
#' @examples
#' palette = palette_extract(regatta)
#' im = palette_visualize(regatta, palette$df, "bar")
#' plot(im)
#' im2 = palette_visualize(regatta, palette$df, "circle")
#' plot(im2)
#' @export
palette_visualize = function( im, palette, type = "bar", threshold = 1, monospace = FALSE, width, size,
                              outline_width = 0, outline_color = c(0.5,0.5,0.5), margin = 0 ){
  pal = palette
  pal = palette[palette$percent >= threshold, ]
  pal$percent = smart.round( pal$n/sum(pal$n), digits = 3 ) * 100
  if( monospace ){
    pal$percent = rep( 1/nrow(pal), nrow(pal) )
    pal$percent = smart.round( pal$percent, digits = 3 ) * 100
  }
  if( missing( width ) ){
    color_bar_width = 0.15
  } else {
    color_bar_width = width
  }
  if( missing( size ) ){
    size = 1024
  }

  if( type == "circle" ){
    cy = cx = r = floor( size / 2 )
    t1_ = t2_ = 0
    out = nimg(array(1,c(size,size,3)))
    out = paint_arc( out, cy, cx, r, size * color_bar_width/2, 0, 2 * pi, outline_color )
    for( i in 1:nrow(pal) ){
      col = c( pal$R[i], pal$G[i], pal$B[i] )
      t1 = t2_
      t2 = t1 + pal$percent[ i ] * 2 * pi / 100
      out = paint_arc( out, cy, cx, r - outline_width,
                       size * color_bar_width/2 - 2 * outline_width, t1, t2, col)
      t1_ = t1
      t2_ = t2
    }
    if( color_bar_width < 1 ){
      diagonal = 2 * ( size / 2 - color_bar_width/2 * size )
      im2 = im_resize_scale( im, scale = diagonal / im_diagonal( im ) )
      out = im_combine( out, im2, y = (size - im_height(im2))/2, x = (size - im_width(im2))/2 )
    }
    return(out)
  } else if( type == "bar" ){
    width = im_width( im )
    bar = im_mono( c(0,0,0), round( im_height( im ) * color_bar_width ), width )
    w = cumsum( pal$percent ) / 100
    w = c( 1, round( width * w )[ -nrow( pal ) ] + 1 )
    for( i in 1:nrow( pal ) ){
      if( w[ i ] > width ){
        break
      }
      bar[, w[ i ]:width, 1] = pal$R[ i ]
      bar[, w[ i ]:width, 2] = pal$G[ i ]
      bar[, w[ i ]:width, 3] = pal$B[ i ]
    }
    if( outline_width >= 1 ){
      bar[c(1:outline_width, (im_height(bar) - outline_width + 1):im_height(bar)),,] = outline_color
    }
    if( margin > 0 ){
      margin = clamping( margin )
      blank = im_mono( c(1,1,1), round( im_height( im ) * margin ), width )
      im = im_combine( im, blank, y = im_height( im ) )
    }
    out = im_combine( im, bar, y = im_height( im ) )
    return(out)
  }
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Composition ----

#' Analyze image symmetry
#' @param im an image
#' @param method either "luminance", "edge", or "both"
#' @param direction either "horizontal", "vertical", or "both"
#' @param sample.rate a numeric between 0 and 1
#' @param lowpass.cutoff cutoff frequency for lowpass filtering
#' @param rawdata if FALSE, summary data is returned. if TRUE, rawdata is also returned
#' @examples
#' im = im_resize( regatta, 128 )
#' df = im_symmetry(im)
#' @export
im_symmetry = function( im, method = "both", direction = "both", sample.rate = 0.25, lowpass.cutoff, rawdata = FALSE ){
  #
  if( method == "both" ){
    if( direction == "both" ){
      df = rbind(
        im_symmetry( im, "luminance", "horizontal", sample.rate, lowpass.cutoff, rawdata = TRUE ),
        im_symmetry( im, "luminance", "vertical", sample.rate, lowpass.cutoff, rawdata = TRUE ),
        im_symmetry( im, "edge", "horizontal", sample.rate, lowpass.cutoff, rawdata = TRUE ),
        im_symmetry( im, "edge", "vertical", sample.rate, lowpass.cutoff, rawdata = TRUE )
      )
    } else {
      df = rbind(
        im_symmetry( im, "luminance", direction, sample.rate, lowpass.cutoff, rawdata = TRUE ),
        im_symmetry( im, "edge", direction, sample.rate, lowpass.cutoff, rawdata = TRUE )
      )
    }
    # mutate combined measure
    df2 = df %>%
      dplyr::group_by( .data$direction, .data$position ) %>%
      dplyr::summarise( pearson_r = mean( .data$pearson_r ), .groups = "drop" ) %>%
      dplyr::group_by( .data$direction ) %>%
      dplyr::mutate( peak = .data$position[ which.max( .data$pearson_r ) ] )
    df2 = cbind( data.frame( method = "combined" ), df2 )
    raw = rbind( df, df2 )
    #
    smr = raw %>%
      dplyr::group_by( .data$method, .data$direction ) %>%
      dplyr::summarise( r = max( .data$pearson_r ), position = mean( .data$peak ), .groups = "drop" )
    if( rawdata ){
      return( list( summary = as.data.frame(smr), rawdata = as.data.frame(raw) ) )
    } else {
      return( as.data.frame( smr ) )
    }
  } else if( direction == "both" ){
    raw = rbind(
      im_symmetry( im, method, "horizontal", sample.rate, lowpass.cutoff, rawdata = TRUE ),
      im_symmetry( im, method, "vertical", sample.rate, lowpass.cutoff, rawdata = TRUE )
    )
    smr = raw %>%
      dplyr::group_by( .data$method, .data$direction ) %>%
      dplyr::summarise( r = max( .data$pearson_r ), position = mean( .data$peak ), .groups = "drop" )
    if( rawdata ){
      return( list( summary = as.data.frame(smr), rawdata = as.data.frame(raw) ) )
    } else {
      return( as.data.frame(smr) )
    }
  }

  if( missing( lowpass.cutoff ) ){
    if( method == "luminance" ){
      lowpass.cutoff = 4
    } else if( method == "edge" ){
      lowpass.cutoff = 10
    }
  }

  # extend the image before filtering to avoid the filtering artifact at image borders
  if( lowpass.cutoff > 0 ){
    margin = ifelse( lowpass.cutoff > 10, 10, round( 100 / lowpass.cutoff ) )
    im %>%
      im_pad( margin, method = "mirror" ) %>%
      fft_filter( "lowpass", "gauss", lowpass.cutoff ) %>%
      im_crop( margin ) -> im
  }

  if( method == "luminance" ){
    im = get_L( im ) %>% clamping
  } else if( method == "edge" ){
    im = get_L( im )
    im = edge_sobel( im )$magnitude
  }

  M = ifelse( direction == "horizontal", im_width( im ), im_height( im ) )
  n.sample = round( sample.rate * M )
  index = seq( from = 2, to = M - 1, length.out = n.sample ) %>% ceiling %>% clamping( 2, M - 1 )
  r = rep( 0, length( index ) )
  npix = im_npix( im )
  for( i in 1:length( index ) ){
    bin_half_width = min( ( index[ i ] - 1 ), abs( index[ i ] - M ) )
    if( direction == "horizontal" ){
      im1 = im[ , (index[i] - bin_half_width):(index[i]-1), ] %>% as.numeric
      im2 = im[ , (index[i]+1):(index[i] + bin_half_width), ,drop = FALSE ] %>%
        nimg %>% im_flip( direction ) %>% as.numeric
    } else {
      im1 = im[ (index[i] - bin_half_width):(index[i]-1),, ] %>% as.numeric
      im2 = im[ (index[i]+1):(index[i] + bin_half_width),,,drop = FALSE ] %>%
        nimg %>% im_flip( direction ) %>% as.numeric
    }
    if( stats::sd( im1 ) == 0 || stats::sd( im2 ) == 0 ){
      r[ i ] = NA
    } else {
      weight = 2 * length( im1 ) / npix
      r[ i ] = stats::cor( im1, im2 ) * weight
    }
  }
  df = data.frame( method = method, direction = direction, position = index, pearson_r = r )
  df$peak = df$position[ which.max( df$pearson_r ) ]

  return( df )

  # im = regatta
  # im = im_resize( im, 128 )
  # df = im_symmetry( im, method = c( "luminance", "edge", "both" )[ 1 ], direction = "both", sample.rate = 0.25, rawdata = T )
  #
  # # image
  # ggplot( as.data.frame( im ), aes( x, y ) ) +
  #   geom_raster( aes( fill = color ) ) +
  #   scale_fill_identity() +
  #   coord_fixed() +
  #   scale_y_continuous( trans = "reverse", expand = c( 0, 0 ) ) +
  #   scale_x_continuous( expand = c( 0, 0 ) ) +
  #   cowplot::theme_cowplot( 14 ) +
  #   theme(
  #     axis.title = element_blank()
  #   ) -> fig_im
  #
  # df = df$rawdata
  # # horizontal
  # ggplot( df %>% subset( direction == "horizontal"),
  #         aes( x = position, y = pearson_r, group = method, colour = method, size = method ) ) +
  #   geom_hline( yintercept = 0, size = 0.6, color = "gray20" ) +
  #   geom_line() +
  #   # geom_point() +
  #   geom_vline( xintercept = df$position[ which.max( df$pearson_r[ df$method == "combined"  & df$direction == "horizontal" ] ) ],
  #               color = "gray30",  size = 1.5 ) +
  #   geom_vline( xintercept = df$position[ which.max( df$pearson_r[ df$method == "edge"      & df$direction == "horizontal" ] ) ],
  #               color = "#FA6A6E", size = 0.8 ) +
  #   geom_vline( xintercept = df$position[ which.max( df$pearson_r[ df$method == "luminance" & df$direction == "horizontal" ] ) ],
  #               color = "#82B9C7", size = 0.8 ) +
  #   scale_color_manual( values = c( "gray30", "#FA6A6E", "#82B9C7" ) ) +
  #   scale_size_manual( values = c( 1.5, 1, 1 ) ) +
  #   scale_x_continuous( expand = c( 0, 0 ) ) +
  #   scale_y_continuous( limits = c( -1, 1 ), breaks = -4:4 * 0.5, expand = c( 0, 0 ) ) +
  #   cowplot::theme_cowplot( 15 ) +
  #   theme(
  #     legend.position = "none",
  #     aspect.ratio = .5,
  #     panel.grid.major.y = element_line(colour = "gray80" )
  #   ) -> fig_h
  #
  # # vertical
  # ggplot( df %>% subset( direction == "vertical"),
  #         aes( x = position, y = pearson_r, group = method, colour = method, size = method ) ) +
  #   geom_hline( yintercept = 0, size = 0.6, color = "gray20" ) +
  #   geom_line() +
  #   # geom_point() +
  #   geom_vline( xintercept = df$position[ which.max( df$pearson_r[ df$method == "combined"  & df$direction == "vertical" ] ) ],
  #               color = "gray30",  size = 1.5 ) +
  #   geom_vline( xintercept = df$position[ which.max( df$pearson_r[ df$method == "edge"      & df$direction == "vertical" ] ) ],
  #               color = "#FA6A6E", size = 0.8 ) +
  #   geom_vline( xintercept = df$position[ which.max( df$pearson_r[ df$method == "luminance" & df$direction == "vertical" ] ) ],
  #               color = "#82B9C7", size = 0.8 ) +
  #   scale_color_manual( values = c( "gray30", "#FA6A6E", "#82B9C7" ) ) +
  #   scale_size_manual( values = c( 1.5, 1, 1 ) ) +
  #   scale_x_reverse( expand = c( 0, 0 ) ) +
  #   scale_y_continuous( limits = c( -1, 1 ), breaks = -4:4 * 0.5, expand = c( 0, 0 ) ) +
  #   cowplot::theme_cowplot( 15 ) +
  #   coord_flip() +
  #   theme(
  #     # legend.position = "none",
  #     legend.position = c( 0.1, 1.4 ),
  #     aspect.ratio = 2 + 2 * ( im_height( im ) - im_width( im ) ) / im_width( im ), #2.5,
  #     panel.grid.major.x = element_line(colour = "gray80" )
  #   ) -> fig_v
  #
  # # plot
  # spacer = ggplot() + coord_fixed() + theme_void()
  # fig = patchwork::wrap_plots( list( fig_h, spacer, fig_im, fig_v ), nrow = 2 )
  # plot( fig )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Complexity ----

#' Edge density of an image
#' @param im an image
#' @param method edge detection method. either "diff", "sobel", "prewitt", or "canny".
#' @return a numeric value
#' @examples
#' edge_density(regatta)
#' @export
edge_density = function( im, method = "sobel" ){
  # img = im_gray( im )
  img = get_L( im )
  if( method == "diff" ){
    edge = edge_diff( img )$magnitude
  } else if( method == "sobel" ){
    edge = edge_sobel( img )$magnitude
  } else if( method == "prewitt" ){
    edge = edge_prewitt( img )$magnitude
  } else if( method == "canny" ){
    edge = edge_canny( img )
  }
  return( mean( edge ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Fractal ----

#' Calculate the fractal dimension of an image using the traditional 2D box counting method
#' @param im an image
#' @param n number of samples
#' @param thr threshold for binarization. either numeric (e.g., 0.5), or "auto", or a string for quantiles (e.g., "15%")
#' @param rawdata if TRUE, raw data is returned as well as slope value
#' @return a numeric
#' @examples
#' Fractal2D(regatta)
#' @export
Fractal2D = function( im, n = 20, thr = "auto", rawdata = FALSE ){
  im2 = im_threshold( im_crop_square( im ), thr, approx = TRUE, adjust = 1 ) %>% get_R

  M = min( im_size( im2 ) )
  S = ( logspace( 2, floor( M / 2 ), n ) )
  # S = c( 2,4,8,16,32,64,128,256 )

  Nr = rep( 0, length( S ) )
  for( i in 1:length( S ) ){
    s = S[ i ]
    Ny = ceiling( im_height( im2 ) / s ) # number of box in y axis
    Nx = ceiling( im_width( im2 ) / s )
    if( i == length( S ) ){
      Ny = 2
      Nx = 2
    }
    nr = 0
    for( y in 1:Ny ){
      for( x in 1:Nx ){
        yy = floor( 1 + ( y - 1 ) * s ):( min( round( y * s ), M ) )
        xx = floor( 1 + ( x - 1 ) * s ):( min( round( x * s ), M ) )
        if( any( im2[ yy, xx, 1 ] == 0 ) ){
          nr = nr + 1
        }
      }
    }
    Nr[ i ] = nr
  }

  invR = M / S
  df = data.frame( Log10_inverseR = log( invR ), Log10_Nr = log( Nr ) )
  df = stats::na.omit( df )
  df = df[is.finite(rowSums(df)),]
  fit = stats::lm( Log10_Nr ~ Log10_inverseR, data = df )
  slope = unname( fit$coefficients[ 2 ] )
  df$slope = as.numeric( sprintf( "%1.3f", slope ) )

  # fig = ggplot( df, aes_string( x = "Log10_inverseR", y = "Log10_Nr" ) ) +
  #   geom_point() +
  #   geom_smooth( method = "lm", formula = y ~ x, se = FALSE ) +
  #   annotate( "text", x = 1.8, y = 7, label = sprintf("Slope = %1.3f", slope), size = 8 )
  # plot( fig )

  if( rawdata ){
    return( df )
  } else {
    return( slope )
  }
}


#' Calculate the fractal dimension of an image using 3D box counting methods
#' @param im an image
#' @param method either "DBC", "RDBC", or "IDBC"
#' @param n number of samples
#' @param rawdata if TRUE, raw data is returned as well as slope value
#' @return a numeric value or a data frame
#' @examples
#' DBC(regatta)
#' @export
DBC = function( im, method = "DBC", n = 20, rawdata = FALSE ){
  im = im_gray( im )
  im = im_crop_square( im )
  im = ceiling( im * 256 )

  M = min( im_size( im ) )

  if( method == "CDBC" ){
    minpow = ceiling( log2( M^(1/3) ) )
    bmin = 2^minpow;
    bmax = bmin;
    while( ( ceiling( M / bmax ) + 1 ) <= ceiling( M / ( bmax - 1 ) ) ){
      bmax = bmax + 1;
    }
    boxes = seq( from = bmin, to = bmax, by = 2 )
    S = boxes
  } else {
    # Smax = floor( M / 2 )
    # N = c( 2^( 1:floor( log2( Smax ) ) ) )
    # S = rev( floor( M / N ) )
    # S = N
    S = logspace( 2, floor( M / 2 ), n )
  }

  Nr = rep( 0, length( S ) )
  for( i in 1:length( S ) ){
    Nr[ i ] = FD_nr( im, S[ i ], method )
  }

  invR = M / S
  df = data.frame( Log10_inverseR = log( invR ), Log10_Nr = log( Nr ) )
  df = stats::na.omit( df )
  df = df[is.finite(rowSums(df)),]
  fit = stats::lm( Log10_Nr ~ Log10_inverseR, data = df )
  slope = unname( fit$coefficients[ 2 ] )
  df$slope = as.numeric( sprintf( "%1.3f", slope ) )

  # fig = ggplot( df, aes_string( x = "Log10_inverseR", y = "Log10_Nr" ) ) +
  #   geom_point() +
  #   geom_smooth( method = "lm", formula = y ~ x, se = FALSE ) +
  #   annotate( "text", x = 1.8, y = 7, label = sprintf("Slope = %1.3f", df$slope[1] ), size = 8 )
  # plot( fig )

  if( rawdata ){
    return( df )
  } else {
    return( slope )
  }
}


FD_nr = function( im, s, method ){
  M = min( im_size( im ) )
  r = s / M
  Ny = ceiling( im_height( im ) / s )
  Nx = ceiling( im_width( im ) / s )
  if( abs( r - 0.5 ) < 0.0001 ){
    Ny = 2
    Nx = 2
  }
  G = 256
  sprime = G * s / M

  if( method == "Li2009" ){
    a = 3
    rprime = r / ( 1 + 2 * a * stats::sd( im ) )
  }
  Nr = 0
  for( y in 1:Ny ){
    for( x in 1:Nx ){
      yy = floor( 1 + ( y - 1 ) * s ):( min( round( y * s ), M ) )
      xx = floor( 1 + ( x - 1 ) * s ):( min( round( x * s ), M ) )
      if( method == "DBC" ){
        l = ceiling( max( im[ yy, xx, 1 ] ) / r / 256 ) %>% clamping( 1, min( Ny, Nx ) )
        k = ceiling( min( im[ yy, xx, 1 ] ) / r / 256 ) %>% clamping( 1, min( Ny, Nx ) )
        nr = l - k + 1
      } else if( method == "RDBC" ){
        dr = max( im[ yy, xx, 1 ] ) - min( im[ yy, xx, 1 ] )
        nr = ifelse( dr == 0, 1, ceiling( dr / sprime ) )
      } else if( method == "Li2009" ){
        Imax = max( im[ yy, xx, 1 ] )
        Imin = min( im[ yy, xx, 1 ] )
        if( Imax == Imin ){
          nr = 1
        } else {
          nr = ceiling( ( Imax - Imin ) / rprime )
        }
      } else if( method == "CDBC" ){
        l = max( im[ yy, xx, 1 ] )
        k = min( im[ yy, xx, 1 ] )
        if( l == k ){
          nr = 1
        } else {
          nr = ceiling( ( l - k ) / sprime )
        }
      } else if( method == "IDBC" ){
        Imax = max( im[ yy, xx, 1 ] )
        Imin = min( im[ yy, xx, 1 ] )
        if( Imax == Imin ){
          nr = 1
        } else {
          nr = ceiling( ( Imax - Imin + 1 ) / sprime )
        }
        nr_old = nr
        # shift
        dy = ifelse( y == Ny, -1, 1 )
        dx = ifelse( x == Nx, -1, 1 )
        yy = yy + dy
        xx = xx + dx
        yy = yy[ yy <= im_height(im) ]
        xx = xx[ xx <= im_width(im) ]
        Imax = max( im[ yy, xx, 1 ] )
        Imin = min( im[ yy, xx, 1 ] )
        if( Imax == Imin ){
          nr = 1
        } else {
          nr = ceiling( ( Imax - Imin + 1 ) / sprime )
        }
        nr = max( nr, nr_old )
      } else if( method == "IMDBC" ){
        Imax = ceiling( max( im[ yy, xx, 1 ] ) / r / 256 )
        Imin = ceiling( min( im[ yy, xx, 1 ] ) / r / 256 )
        if( Imax == Imin ){
          nr = ifelse( Imax == 0, 0, 1 )
        } else {
          h = r * ( Imax - Imin - 1 )
          # h = ifelse( h == 0, 1, h )
          nr = ceiling( ( Imax - Imin + 1 ) / h )
        }
        nr_old = nr
        # shift
        dy = ifelse( y == Ny, -1, 1 )
        dx = ifelse( x == Nx, -1, 1 )
        yy = yy + dy
        xx = xx + dx
        Imax = ceiling( max( im[ yy, xx, 1 ] ) / r / 256 )
        Imin = ceiling( min( im[ yy, xx, 1 ] ) / r / 256 )
        if( Imax == Imin ){
          nr = ifelse( Imax == 0, 0, 1 )
        } else {
          h = r * ( Imax - Imin - 1 )
          # h = ifelse( h == 0, 1, h )
          nr = ceiling( ( Imax - Imin + 1 ) / h )
        }
        nr = max( nr, nr_old )
      }
      Nr = Nr + nr
    }
  }

  return( Nr )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# PHOG analysis ----

#' Get a gradient image
#' @param im an image
#' @param starts.horizontal axis direction
#' @param clockwise direction of rotation
#' @return a list of images
#' #' @examples
#' G = get_gradient(regatta)
#' plot(G$theta)
#' plot(G$magnitude)
#' @export
get_gradient = function( im, starts.horizontal = TRUE, clockwise = TRUE ){
  im2 = im_pad( im, n = 1, method = "mirror" )
  gy = im_shift( im2, axis = "y", lag = -1 ) - im_shift( im2, axis = "y", lag = 1 )
  gx = im_shift( im2, axis = "x", lag = -1 ) - im_shift( im2, axis = "x", lag = 1 )
  gy = im_crop( gy, rep( 1, 4 ) ) # gy %>% visualize_contrast %>% plot
  gx = im_crop( gx, rep( 1, 4 ) ) # gx %>% visualize_contrast %>% plot
  if( starts.horizontal ){
    if( clockwise ){
      theta = atan2( gx, gy ) # The way this argument is given leads to an intuitive angle assignment
    } else {
      theta = atan2( gx, -gy ) # match to Redies
    }
  } else {
    theta = atan2( gy, -gx )
  }
  magnitude = sqrt( gy^2 + gx^2 ) # magnitude %>% plot( rescale = TRUE )
  return( list( theta = theta, magnitude = magnitude ) )
}


get_gradient_lab = function( im, starts.horizontal = TRUE, clockwise = TRUE ){
  im2 = sRGB2Lab( im )
  im2[,,1] = ( im2[,,1] / 100 ) %>% clamping
  ablimit = 100
  im2[,,2] = ( ( im2[,,2] + ablimit ) / ( 2 * ablimit ) ) %>% clamping()
  im2[,,3] = ( ( im2[,,3] + ablimit ) / ( 2 * ablimit ) ) %>% clamping()
  im3 = im2
  # get the max value across channels
  for( y in 1:im_height( im2 ) ){
    for( x in 1:im_width( im2 ) ){
      im3[ y, x, 1 ] = ifelse( abs( im2[ y, x, 1 ] ) > abs( im2[ y, x, 2 ] ), im2[ y, x, 1 ], im2[ y, x, 2 ] )
      im3[ y, x, 1 ] = ifelse( abs( im3[ y, x, 1 ] ) > abs( im2[ y, x, 3 ] ), im3[ y, x, 1 ], im2[ y, x, 3 ] )
    }
  }
  im4 = nimg( im3[,,1] )
  return( get_gradient( im4, starts.horizontal, clockwise ) )
}


#' Calculate PHOG descriptor
#' @param gradient gradient data
#' @param n.bin number of orientation/direction bins
#' @param is.orientation TRUE for orientation (0-180 degrees), FALSE for direction (0-360 degrees)
#' @param starts.horizontal if TRUE, the first bin is centered at 0 degree
#' @return a data frame
get_hog = function( gradient, n.bin = 8, is.orientation = TRUE, starts.horizontal = TRUE ){
  theta = gradient$theta
  if( is.orientation ){
    theta[ theta < 0 ] = theta[ theta < 0 ] + pi # direction to orientation # %>% plot( theta, rescale = T )
    bin_width = pi / n.bin
    theta_bin = ceiling( ( ( theta + bin_width / 2 ) %% pi ) / bin_width ) # plot(theta_bin, rescale = T)
  } else {
    theta = theta + pi # theta %>% plot( rescale = TRUE )
    bin_width = 2 * pi / n.bin
    theta_bin = ceiling( ( ( theta + bin_width / 2 ) %% ( 2 * pi ) ) / bin_width ) # plot(theta_bin, rescale = T)
  }
  # bin = theta_bin * magnitude # plot(bin, rescale = T)
  hog = rep( 0, n.bin )
  for( i in 1:n.bin ){
    hog[ i ] = sum( gradient$magnitude[ theta_bin == i ] )
  }
  hog_normalized = hog
  if( sum( hog ) != 0 ){
    hog_normalized = hog / sum( hog )
  }
  margin = bin_width * 180 / pi / 2
  if( starts.horizontal ){
    angle = seq( from = 0, by = bin_width * 180 / pi, length.out = n.bin )
    lower = round( angle - margin )
    upper = round( angle + margin )
    df = data.frame( lower = lower, upper = upper, angle = angle, value = hog_normalized, value_nn = hog )
  } else {
    angle = seq( from = 90, by = bin_width * 180 / pi, length.out = n.bin ) %% 180
    lower = round( angle - margin )
    upper = round( angle + margin )
    df = data.frame(
      lower = lower[ order( angle ) ], upper = upper[ order( angle ) ],
      angle = angle[ order( angle ) ], value = hog_normalized[ order( angle ) ],
      value_nn = hog[ order( angle ) ]
    )
  }
  return( df )
}


#' Calculate PHOG descriptor
#' @param gradient gradient data or input image
#' @param level number of PHOG pyramid levels
#' @param n.bin number of orientation/direction bins
#' @param is.orientation TRUE for orientation (0-180 degrees), FALSE for direction (0-360 degrees)
#' @param starts.horizontal if TRUE, the first bin is centered at 0 degree
#' @param clockwise direction of rotation
#' @return a data frame
#' @examples
#' df = PHOG_descriptor(regatta, level = 2)
#' @export
PHOG_descriptor = function( gradient, level = 3, n.bin = 8, is.orientation = TRUE, starts.horizontal = TRUE, clockwise = FALSE ){
  if( is.nimg( gradient ) ){
    gradient = get_gradient( get_L( gradient ), starts.horizontal, clockwise )
  }
  g_theta = gradient$theta
  g_magnitude = gradient$magnitude
  # ground level
  hog_global = get_hog( gradient, n.bin, is.orientation, starts.horizontal )
  df = cbind( data.frame( level = 0, y = 1, x = 1 ), hog_global, data.frame( HI = NA ) )
  if( level > 0 ){
    # each level
    height = im_height( g_theta )
    width = im_width( g_theta )
    for( l in 1:level ){
      cellsize = c( height / 2^l, width / 2^l )
      # each block
      for( x in 1:2^l ){
        for( y in 1:2^l ){
          y1 = 1 + round( ( y - 1 ) * cellsize[ 1 ] )
          x1 = 1 + round( ( x - 1 ) * cellsize[ 2 ] )
          y2 = min( height, y1 + round( cellsize[ 1 ] ) - 1 )
          x2 = min( width,  x1 + round( cellsize[ 2 ] ) - 1 )
          theta = im_get( g_theta, y1, x1, y2, x2 )
          magni = im_get( g_magnitude, y1, x1, y2, x2 )
          hog = get_hog( list( theta = theta, magnitude = magni ), n.bin, is.orientation, starts.horizontal )
          d = cbind( data.frame( level = l, y = y, x = x ), hog )
          # Histgram intersection
          HI = sum( pmin( hog_global$value, hog$value ) )
          m1 = mean( hog_global$value_nn )
          m2 = mean( hog$value_nn ) * 2^( 2 * l )
          if( m1 < 0.00000001 || m2 < 0.00000001 ){
            strength = 0
          } else if( m1 > m2 ){
            strength = m2 / m1
          } else {
            strength = m1 / m2
          }
          d$HI = HI * strength
          df = rbind( df, d )
        }
      }
    }
  }
  return( df )
}


#' Calculate PHOG features
#' @param im an image
#' @param level number of PHOG pyramid levels
#' @param n.bin number of orientation/direction bins
#' @param is.orientation TRUE for orientation (0-180 degrees), FALSE for direction (0-360 degrees)
#' @param starts.horizontal if TRUE, the first bin is centered at 0 degree
#' @param clockwise direction of rotation
#' @param level.weight relative weights to each level for calculating self-similarity
#' @return a data frame
#' @examples
#' PHOG_features(regatta)
#' @export
PHOG_features = function( im, level = 3, n.bin = 8, is.orientation = TRUE, starts.horizontal = TRUE, clockwise = FALSE,
                          level.weight = c( 1, 1, 1 ) ){
  # normalize weights
  level.weight = level.weight[ 1:level ]
  level.weight = level.weight / sum( level.weight )

  G = get_gradient( get_L( im ), starts.horizontal, clockwise )
  phog = PHOG_descriptor( G, level, n.bin, is.orientation, starts.horizontal, clockwise )

  phog %>%
    dplyr::filter( level > 0 ) %>%
    dplyr::group_by( level, y, x ) %>%
    dplyr::summarise( HI = mean( .data$HI ), .groups = "drop" ) %>%
    dplyr::group_by( level ) %>%
    dplyr::summarise( HI = median( .data$HI ), .groups = "drop" ) %>%
    dplyr::mutate( weight = level.weight, HI2 = .data$HI * .data$weight ) -> df2
  # plot( phog$value[ phog$level == 0 ], type = "h", ylim = c(0,1) )
  # plot( phog$value[ phog$level == 1 ], type = "h", ylim = c(0,1) )
  # plot( phog$value[ phog$level == 2 ], type = "h", ylim = c(0,1) )
  data.frame(
    self_similarity = sum( df2$HI2 ),
    complexity = 100 * mean( G$magnitude ),
    anisotropy = 100 * stats::sd( phog$value[ phog$level == level ] )
  ) %>% return
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Photoshop ----


#' Color dodge
#' @param bottom an image
#' @param top an image
#' @return an image
#' @examples
#' plot(color_dodge(regatta, regatta^10))
#' @export
color_dodge = function( bottom, top = bottom ){
  return( clamping( bottom / ( 1 - top ) ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Visualization ----


#' Visualize CIELAB image
#' @param im an RGB image
#' @param scale either TRUE or FALSE (default)
#' @return a list of images
#' @export
CIELAB_visualize = function( im, scale = FALSE ){
  lab = sRGB2Lab( im )
  L = get_R( lab )
  A = get_G( lab )
  B = get_B( lab )
  ab_max = max( abs( A ), abs( B ) )

  LM = array( mean( L ), dim = dim( L ) )
  C0 = array( 0, dim = dim( L ) )
  if( scale ){
    A = clamping( A * 90 / ab_max, -98, 98 )
    B = clamping( B * 90 / ab_max, -98, 98 )
  }
  im_L = merge_color( list( L, C0, C0 ) ) %>% Lab2sRGB
  im_a = merge_color( list( LM, A, C0 ) ) %>% Lab2sRGB
  im_b = merge_color( list( LM, C0, B ) ) %>% Lab2sRGB
  return( list( L = im_L, a = im_a, b = im_b ) )
}


visualize_contrast = function( im, abs.range = NULL, Lcenter = 55 ){
  if( is.null( abs.range ) ){
    abs.range = max( abs( im ) )
  }
  L = clamping( Lcenter + im * ( 100 - Lcenter ) / abs.range, 0, 100 )
  ab = array( 0, dim = dim( L ) )
  clamping( Lab2sRGB( merge_color( list( L, ab, ab ) ) ) )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Color transfer ----


RGB2LMS = function( im, sRGB = TRUE ){
  if( sRGB ){
    im = sRGB2RGB( im )
  }
  L = 0.3811 * get_R( im ) + 0.5783 * get_G( im ) + 0.0402 * get_B( im )
  M = 0.1967 * get_R( im ) + 0.7244 * get_G( im ) + 0.0782 * get_B( im )
  S = 0.0241 * get_R( im ) + 0.1288 * get_G( im ) + 0.8444 * get_B( im )
  return( merge_color( list( L, M, S ) ) )
}


LMS2RGB = function( im, sRGB = TRUE ){
  R =  4.4679 * get_R( im ) - 3.5873 * get_G( im ) + 0.1193 * get_B( im )
  G = -1.2186 * get_R( im ) + 2.3809 * get_G( im ) - 0.1624 * get_B( im )
  B =  0.0497 * get_R( im ) - 0.2439 * get_G( im ) + 1.2045 * get_B( im )
  im = merge_color( list( R, G, B ) )
  if( sRGB ){
    im = RGB2sRGB( im )
  }
  return( im )
}


LMS2lab = function( im, epsilon = 0.0001 ){
  im = log10( im + epsilon )
  l = 1/sqrt(3) * get_R( im ) + 1/sqrt(3) * get_G( im ) + 1/sqrt(3) * get_B( im )
  a = 1/sqrt(6) * get_R( im ) + 1/sqrt(6) * get_G( im ) - 2/sqrt(6) * get_B( im )
  b = 1/sqrt(2) * get_R( im ) - 1/sqrt(2) * get_G( im )
  return( merge_color( list( l, a, b ) ) )
}


lab2LMS = function( im, epsilon = 0.0001 ){
  L = sqrt(3)/3 * get_R( im ) + sqrt(6)/6 * get_G( im ) + sqrt(2)/2 * get_B( im )
  M = sqrt(3)/3 * get_R( im ) + sqrt(6)/6 * get_G( im ) - sqrt(2)/2 * get_B( im )
  S = sqrt(3)/3 * get_R( im ) - sqrt(6)/3 * get_G( im )
  L = 10^L - epsilon
  M = 10^M - epsilon
  S = 10^S - epsilon
  return( merge_color( list( L, M, S ) ) )
}


color_transfer = function( im, ref, sRGB = TRUE ){
  if( is.list( ref ) ){
    Reinhard_multiple( im, ref, sRGB )
  } else {
    Reinhard_single( im, ref, sRGB )
  }
}


Reinhard_single = function( im, ref, sRGB = TRUE, out.lab = FALSE ){
  im = im_tricolored( im )
  ref = im_tricolored( ref )

  lab_s = LMS2lab( RGB2LMS( im, sRGB ) )
  lab_t = LMS2lab( RGB2LMS( ref, sRGB ) )
  mmt_s = im_moments( lab_s, channel = 1:3, order = 1:2, space = "RGB", max_size = 1024 )
  mmt_t = im_moments( lab_t, channel = 1:3, order = 1:2, space = "RGB", max_size = 1024 )

  l = get_channel( lab_s, 1 )
  a = get_channel( lab_s, 2 )
  b = get_channel( lab_s, 3 )
  l = l - mean( l )
  a = a - mean( a )
  b = b - mean( b )
  l = mmt_t$value[ 2 ] / mmt_s$value[ 2 ] * l + mmt_t$value[ 1 ]
  a = mmt_t$value[ 4 ] / mmt_s$value[ 4 ] * a + mmt_t$value[ 3 ]
  b = mmt_t$value[ 6 ] / mmt_s$value[ 6 ] * b + mmt_t$value[ 5 ]
  lab = merge_color( list( l, a, b ) )
  if( out.lab ){
    return( lab )
  }
  out = LMS2RGB( lab2LMS( lab ), sRGB )
  out = clamping( out )
  return( out )
}


Reinhard_multiple = function( im, ref, sRGB = TRUE ){
  im = im_tricolored( im )
  ref = lapply( ref, im_tricolored )

  lab_s = LMS2lab( RGB2LMS( im, sRGB ) )
  lab_t = lapply( ref, RGB2LMS, sRGB )
  lab_t = lapply( lab_t, LMS2lab )
  mmt_t = lapply( lab_t, im_moments, 1:3, 1:2, "RGB", 1024 )

  trans = list()
  for( i in 1:length( ref ) ){
    trans = c( trans, list( Reinhard_single( im, ref[[ i ]], sRGB, out.lab = TRUE) ) )
  }

  # distance
  weight = list()
  w_sum = 0
  for( i in 1:length( ref ) ){
    means = array(
      c( rep( mmt_t[[ i ]]$value[ 1 ], prod( im_size( im ) ) ),
         rep( mmt_t[[ i ]]$value[ 3 ], prod( im_size( im ) ) ),
         rep( mmt_t[[ i ]]$value[ 5 ], prod( im_size( im ) ) ) ),
      dim = dim( im ) )
    SDs = array(
      c( rep( mmt_t[[ i ]]$value[ 2 ], prod( im_size( im ) ) ),
         rep( mmt_t[[ i ]]$value[ 4 ], prod( im_size( im ) ) ),
         rep( mmt_t[[ i ]]$value[ 6 ], prod( im_size( im ) ) ) ),
      dim = dim( im ) )
    distance = abs( lab_s - means ) / SDs
    weight = c( weight, list( 1 / distance ) )
    w_sum = w_sum + 1 / distance
  }

  out = 0
  for( i in 1:length( ref ) ){
    out = out + weight[[ i ]] / w_sum * trans[[ i ]]
  }
  out = LMS2RGB( lab2LMS( out ), sRGB )
  out = clamping( out )
  return( out )
}


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# CFS ----


#' Create a 'dead-leaves' image
#' @param height image height
#' @param width image width
#' @param shape the shape of leaves. either "square" (default) or "disk"
#' @param grayscale either FALSE (default, color output) or TRUE (grayscale output)
#' @param sigma_n numeric
#' @param rmin minimum diameter of each leaf
#' @param rmax maximum diameter of each leaf
#' @return an image
#' @examples
#' plot(create_dead_leaves(128))
#' @export
create_dead_leaves = function( height, width = height, shape = "square", grayscale = FALSE,
                               sigma_n = 2, rmin = 0.02, rmax = 0.4 ){
  k = 100
  r_list = seq(
    from = rmin * max( width, height ), to = rmax * max( width, height ), length.out = k )
  r_dist = 1 / r_list^sigma_n
  if( sigma_n > 0 ){
    r_dist = r_dist - 1 / ( rmax * max( width, height ) )^sigma_n
  }
  r_dist = rescaling01( cumsum( r_dist ) )

  n_iter = 3000
  M = array( -1, dim = c( height, width, ifelse( grayscale, 1, 3 ) ) )
  m = width * height
  for( i in 1:n_iter ){
    col = c( runif( 1 ), runif( 1 ), runif( 1 ) )[ 1:dim( M )[ 3 ] ]
    r = r_list[ which.min( abs( runif( 1 ) - r_dist ) ) ]
    cx = runif( 1, 1, width )
    cy = runif( 1, 1, height )
    u = matrix( 1:width - cx, ncol = width, nrow = height, byrow = T )
    v = matrix( 1:height - cy, ncol = width, nrow = height, byrow = F )

    if( shape == "disk" ){
      D = u^2 + v^2
      I = M[,,1] == -1 & D < r^2
    } else if( shape == "square" ){
      I = M[,,1] == -1 & abs( u ) < r & abs( v ) < r
    }

    M[ I ] = rep( col, each = sum( I ) )

    m = m - sum( I )
    if( m <= 0 ){
      # print( paste0( "break: ", m ) )
      break
    }
  }

  M[ M == -1 ] = 0

  return( nimg( M ) )
}


#' #' Create and save dead-leaves images
#' create_CFS_masks = function(){
#'   height = 128
#'   n_img = 2
#'   for( i in 1:n_img ){
#'     im = create_dead_leaves( height )
#'     im_save( im, sprintf( paste0( "%0", nchar( n_img ), "d" ), i ), "CFS2" )
#'   }
#' }
tsuda16k/pictplot documentation built on March 1, 2023, 8:44 p.m.