# giottoImage creation ####
#' @title createGiottoImage
#' @name createGiottoImage
#' @description Creates a giotto image that can be added to a Giotto object and/or used to add an image to the spatial plotting functions
#' @param gobject giotto object
#' @param spat_unit spatial unit
#' @param spatial_locs spatial locations (alternative if \code{gobject = NULL})
#' @param spat_loc_name name of spatial locations within gobject
#' @param mg_object magick image object
#' @param name name for the image
#' @param image_transformations vector of sequential image transformations
#' @param negative_y Map image to negative y spatial values if TRUE during automatic
#' alignment. Meaning that origin is in upper left instead of lower left.
#' @param do_manual_adj flag to use manual adj values instead of automatic alignment when given a gobject or spatlocs
#' @param xmax_adj,xmin_adj,ymax_adj,ymin_adj adjustment of the maximum or maximum x or y-value to align the image
#' @param scale_factor scaling of image dimensions relative to spatial coordinates
#' @param x_shift,y_shift shift image along x or y axes
#' @param scale_x,scale_y independently scale image in x or y direction
#' @param order perform scaling or adjustments and shifts first
#' @param xmin_set,xmax_set,ymin_set,ymax_set values to override image minmax spatial anchors when doing adjustments
#' @param verbose be verbose
#' @details image_transformations: transformation options from magick library
#' [\strong{flip_x_axis}] flip x-axis (\code{\link[magick]{image_flop}})
#' [\strong{flip_y_axis}] flip y-axis (\code{\link[magick]{image_flip}})
#' Example: image_transformations = c(flip_x_axis, flip_y_axis); first flip x-axis and then y-axis
#' @return a giottoImage object
#' @export
createGiottoImage = function(gobject = NULL,
spat_unit = NULL,
spatial_locs = NULL,
spat_loc_name = NULL,
mg_object,
name = 'image',
image_transformations = NULL,
negative_y = TRUE,
do_manual_adj = FALSE,
xmax_adj = 0,
xmin_adj = 0,
ymax_adj = 0,
ymin_adj = 0,
scale_factor = 1,
x_shift = NULL,
y_shift = NULL,
scale_x = NULL,
scale_y = NULL,
order = c('first_scale','first_adj'),
xmin_set = NULL,
xmax_set = NULL,
ymin_set = NULL,
ymax_set = NULL,
verbose = TRUE) {
# Check params
order = match.arg(order, choices = c('first_scale','first_adj'))
scale_factor = c(x = scale_factor, y = scale_factor)
# create minimum giotto
g_image = new('giottoImage',
name = name,
mg_object = NULL,
minmax = NULL,
boundaries = NULL,
scale_factor = NULL,
resolution = NULL,
file_path = NULL,
OS_platform = .Platform[['OS.type']])
## 1.a. check magick image object
if(!inherits(mg_object, 'magick-image')) {
if(file.exists(mg_object)) {
g_image@file_path = mg_object
mg_object = try(magick::image_read(mg_object))
if(inherits(mg_object, 'try-error')) {
stop(mg_object, ' can not be read by magick::image_read() \n')
}
} else {
stop("mg_object needs to be an image object 'magick-image' from the magick package or \n
an existing path that can be read by magick::image_read()")
}
}
## 1.b. check colorspace
info = magick::image_info(mg_object)
mg_colorspace = info$colorspace
if(mg_colorspace == 'Gray') {
mg_object = magick::image_convert(mg_object, colorspace = 'rgb')
}
## 1.c. perform transformations if found
if(!is.null(image_transformations)) {
for(transf in image_transformations) {
if(transf == 'flip_x_axis') {
mg_object = magick::image_flop(mg_object)
} else if(transf == 'flip_y_axis') {
mg_object = magick::image_flop(mg_object)
} else {
cat(transf, ' is not a supported transformation, see details \n')
}
}
}
g_image@mg_object = mg_object
## 2. spatial minmax and adjustments -- manual OR by image dimensions (auto)
if(verbose == TRUE) {
if(do_manual_adj == TRUE) cat('do_manual_adj == TRUE \n','Boundaries will be adjusted by given values.\n')
}
# If spatlocs or gobject supplied, minmax values will always be generated
# If do_manual_adj == TRUE, bypass followup automatic boundary value generation
if(!is.null(gobject)) {
# Get spatial locations (or automatically take first available)
spatlocs = get_spatial_locations(gobject = gobject,
spat_unit = spat_unit,
spat_loc_name = spat_loc_name,
copy_obj = FALSE,
output = 'data.table')
# Find g_image minmax (spatial) from spatial_locs in gobject
my_xmin = min(spatlocs$sdimx)
my_xmax = max(spatlocs$sdimx)
my_ymin = min(spatlocs$sdimy)
my_ymax = max(spatlocs$sdimy)
if(do_manual_adj == FALSE) {
# find automatic adjustment values
img_minmax = get_img_minmax(mg_img = mg_object,
negative_y = negative_y)
adj_values = get_adj_rescale_img(img_minmax = img_minmax,
spatial_locs = spatlocs,
scale_factor = scale_factor)
# Automatic g_image@boundaries values
xmax_adj = as.numeric(adj_values[['xmax_adj_orig']])
xmin_adj = as.numeric(adj_values[['xmin_adj_orig']])
ymax_adj = as.numeric(adj_values[['ymax_adj_orig']])
ymin_adj = as.numeric(adj_values[['ymin_adj_orig']])
}
} else if(!is.null(spatial_locs)) {
spatlocs = spatial_locs
if(!all(c('sdimx','sdimy') %in% colnames(spatlocs))) {
stop('spatial_locs needs to be data.frame-like object with a sdimx and sdimy column')
}
# Find g_image minmax (spatial) from spatial_locs argument
my_xmin = min(spatlocs$sdimx)
my_xmax = max(spatlocs$sdimx)
my_ymin = min(spatlocs$sdimy)
my_ymax = max(spatlocs$sdimy)
if(do_manual_adj == FALSE) {
#find auto adjustment values
img_minmax = get_img_minmax(mg_img = mg_object,
negative_y = negative_y)
adj_values = get_adj_rescale_img(img_minmax = img_minmax,
spatial_locs = spatlocs,
scale_factor = scale_factor)
# Automatic g_image@boundaries values
xmax_adj = as.numeric(adj_values[['xmax_adj_orig']])
xmin_adj = as.numeric(adj_values[['xmin_adj_orig']])
ymax_adj = as.numeric(adj_values[['ymax_adj_orig']])
ymin_adj = as.numeric(adj_values[['ymin_adj_orig']])
}
} else {
if(verbose == TRUE) {
warning('gobject or spatial locations are not provided \n',
'Arbitrary values will be given \n')
}
# Default g_image@minmax values if no spatial_locs provided
my_xmin = 0; my_xmax = 10; my_ymin = 0; my_ymax = 10
}
# Set minmax and boundary values for return
g_image@minmax = c('xmax_sloc' = my_xmax,
'xmin_sloc' = my_xmin,
'ymax_sloc' = my_ymax,
'ymin_sloc' = my_ymin)
## if do_manual == TRUE, boundary values are those given or defaulted as arguments
## if do_manual == FALSE, boundary values are taken from automatic processes above
g_image@boundaries = c('xmax_adj' = xmax_adj,
'xmin_adj' = xmin_adj,
'ymax_adj' = ymax_adj,
'ymin_adj' = ymin_adj)
# scale factor and resolution values for return
g_image@scale_factor = scale_factor
g_image@resolution = 1/scale_factor
# Apply any additional manual adjustments through updateGiottoImage
if(do_manual_adj == TRUE) {
if(length(c(x_shift,
y_shift,
scale_x,
scale_y,
xmin_set,
xmax_set,
ymin_set,
ymax_set)) > 0) {
g_image = updateGiottoImageMG(giottoImage = g_image,
return_gobject = FALSE,
xmax_adj = xmax_adj,
xmin_adj = xmin_adj,
ymax_adj = ymax_adj,
ymin_adj = ymin_adj,
x_shift = x_shift,
y_shift = y_shift,
scale_factor = scale_factor,
order = order,
xmin_set = xmin_set,
xmax_set = xmax_set,
ymin_set = ymin_set,
ymax_set = ymax_set,
verbose = FALSE)
}
}
# image object
return(g_image)
}
# giottoLargeImage creation ####
#' @title createGiottoLargeImage
#' @name createGiottoLargeImage
#' @description Creates a large giotto image that can be added to a Giotto subcellular object. Generates deep copy of SpatRaster
#' @param raster_object terra SpatRaster image object
#' @param name name for the image
#' @param negative_y Map image to negative y spatial values if TRUE. Meaning that origin is in upper left instead of lower left.
#' @param extent SpatExtent object to assign spatial extent. Takes priority unless use_rast_ext is TRUE.
#' @param use_rast_ext Use extent from input raster object
#' @param image_transformations vector of sequential image transformations - under construction
#' @param flip_vertical flip raster in a vertical manner
#' @param flip_horizontal flip raster in a horizontal manner
#' @param xmax_bound,xmin_bound,ymax_bound,ymin_bound assign min and max x and y
#' values for image spatial placement
#' @param scale_factor scaling of image dimensions relative to spatial coordinates
#' @param verbose be verbose
#' @return a giottoLargeImage object
#' @export
createGiottoLargeImage = function(raster_object,
name = 'image',
negative_y = TRUE,
extent = NULL,
use_rast_ext = FALSE,
image_transformations = NULL,
flip_vertical = FALSE,
flip_horizontal = FALSE,
xmax_bound = NULL,
xmin_bound = NULL,
ymax_bound = NULL,
ymin_bound = NULL,
scale_factor = 1,
verbose = TRUE) {
# create minimum giotto
g_imageL = new('giottoLargeImage',
name = name,
raster_object = NULL,
overall_extent = NULL,
scale_factor = NULL,
resolution = NULL,
file_path = NULL,
OS_platform = .Platform[['OS.type']])
## 1. check raster object and load as SpatRaster if necessary
if(!inherits(raster_object, 'SpatRaster')) {
if(file.exists(raster_object)) {
g_imageL@file_path = raster_object
raster_object = create_terra_spatRaster(image_path = raster_object)
} else {
stop("raster_object needs to be a'SpatRaster' object from the terra package or \n
an existing path that can be read by terra::rast()")
}
}
# Prevent updates to original raster object input
if(getNamespaceVersion('terra') >= '1.15-12') raster_object = terra::deepcopy(raster_object)
else {
# raster_object = terra::copy(raster_object)
if(isTRUE(verbose)) warning('\n If largeImage was created from a terra raster
object, manipulations to the giotto image may be
reflected in the raster object as well. Update
terra to >= 1.15-12 to avoid this issue. \n')
}
## 2. image bound spatial extent
if(use_rast_ext == TRUE) {
extent = terra::ext(raster_object)
if(verbose == TRUE) cat('use_rast_ext == TRUE, extent from input raster_object will be used.')
}
# By extent object (priority)
if(!is.null(extent)) {
if(inherits(extent, 'SpatExtent')) {
terra::ext(raster_object) = extent
} else {
stop('extent argument only accepts terra SpatExtent objects')
}
} else { # OR by manual OR by image dimensions (auto)
# Check if manual adj values were given
# Assign default values for any that were not manually given
if(all(is.null(xmax_bound),
is.null(xmin_bound),
is.null(ymax_bound),
is.null(ymin_bound))) {
im_dim = dim(raster_object)[2:1]
# Apply scale_factor
im_dim = im_dim * scale_factor
# Automatic extent values
xmax_bound = im_dim[1]
xmin_bound = 0
if(negative_y == TRUE) {
ymax_bound = 0
ymin_bound = -im_dim[2]
} else if(negative_y == FALSE) {
ymax_bound = im_dim[2]
ymin_bound = 0
}
} else {
# Manual extent values
if(is.null(xmax_bound) == TRUE) xmax_bound = 1
if(is.null(xmin_bound) == TRUE) xmin_bound = 0
if(negative_y == TRUE) {
if(is.null(ymax_bound) == TRUE) ymax_bound = 0
if(is.null(ymin_bound) == TRUE) ymin_bound = -1
} else if(negative_y == FALSE) {
if(is.null(ymax_bound) == TRUE) ymax_bound = 1
if(is.null(ymin_bound) == TRUE) ymin_bound = 0
}
}
terra::ext(raster_object) = c(xmin_bound,xmax_bound,ymin_bound,ymax_bound)
}
## transformations
## flip axes ##
if(flip_vertical == TRUE) {
raster_object = terra::flip(raster_object, direction = 'vertical')
}
if(flip_horizontal == TRUE) {
raster_object = terra::flip(raster_object, direction = 'horizontal')
}
## 3. Assign raster_object to giottoLargeImage
g_imageL@raster_object = raster_object
## 4. scale factor and resolution values
g_imageL@resolution = terra::res(g_imageL@raster_object) # (x,y)
names(g_imageL@resolution) = c('x','y')
g_imageL@scale_factor = (1/g_imageL@resolution)
## 5. Get image characteristics by sampling
sampleValues = stats::na.omit(terra::spatSample(raster_object,
size = 5000, # Defines the rough maximum of pixels allowed when resampling
method = 'regular',
value = TRUE))
if(nrow(sampleValues) == 0) {
if(verbose == TRUE) cat('No values discovered when sampling for image characteristics')
} else {
# get intensity range
srMinmax = suppressWarnings(terra::minmax(raster_object))
if(sum(is.infinite(srMinmax)) == 0) { # pull minmax values from terra spatRaster obj if img was small enough for them to be calculated
g_imageL@max_intensity = srMinmax[2]
g_imageL@min_intensity = srMinmax[1]
} else { # pull minmax values from sampled subset if img was too large
intensityRange = range(sampleValues)
g_imageL@max_intensity = intensityRange[2]
g_imageL@min_intensity = intensityRange[1]
}
# find out if image is int or floating point
is_int = identical(sampleValues, round(sampleValues))
if(is_int == TRUE) {
g_imageL@is_int = TRUE
} else {
g_imageL@is_int = FALSE
}
}
## 6. extent object
g_imageL@extent = g_imageL@overall_extent = as.vector(terra::ext(raster_object))
## 7. return image object
return(g_imageL)
}
#' @title createGiottoLargeImageList
#' @name createGiottoLargeImageList
#' @description Creates a list of large giotto images that can be added to a Giotto object. Generates deep copy of SpatRaster
#' @param raster_objects vector of image paths or terra SpatRaster image objects
#' @param names vector of names for the images
#' @param negative_y Map image to negative y spatial values if TRUE. Meaning that origin is in upper left instead of lower left.
#' @param extent SpatExtent object to assign spatial extent. Takes priority unless use_rast_ext is TRUE.
#' @param use_rast_ext Use extent from input raster object
#' @param image_transformations vector of sequential image transformations - under construction
#' @param flip_vertical flip raster in a vertical manner
#' @param flip_horizontal flip raster in a horizontal manner
#' @param xmax_bound,xmin_bound,ymax_bound,ymin_bound assign min and max x and y
#' values for image spatial placement
#' @param scale_factor scaling of image dimensions relative to spatial coordinates
#' @param verbose be verbose
#' @details See \code{\link{createGiottoLargeImage}}
#' @return a list with giottoLargeImage objects
#' @export
createGiottoLargeImageList = function(raster_objects,
names = 'image',
negative_y = TRUE,
extent = NULL,
use_rast_ext = FALSE,
image_transformations = NULL,
flip_vertical = FALSE,
flip_horizontal = FALSE,
xmax_bound = NULL,
xmin_bound = NULL,
ymax_bound = NULL,
ymin_bound = NULL,
scale_factor = 1,
verbose = TRUE) {
l_images = length(raster_objects)
l_image_names = length(unique(names))
if(l_image_names != l_image_names) {
stop('length of raster_objects and unique names must be the same')
}
result_list = list()
for(i in 1:l_images) {
image_res = createGiottoLargeImage(raster_object = raster_objects[[i]],
name = names[[i]],
negative_y = negative_y,
extent = extent,
use_rast_ext = use_rast_ext,
image_transformations = image_transformations,
flip_vertical = flip_vertical,
flip_horizontal = flip_horizontal,
xmax_bound = xmax_bound,
xmin_bound = xmin_bound,
ymax_bound = ymax_bound,
ymin_bound = ymin_bound,
scale_factor = scale_factor,
verbose = verbose)
result_list[[i]] = image_res
}
return(result_list)
}
# giottoImage or magick tools ####
#' @title convert_mgImage_to_array_DT
#' @name convert_mgImage_to_array_DT
#' @description converts a magick image object to a data.table
#' @param mg_object magick image or Giotto image object
#' @return data.table with image pixel information
#' @keywords internal
convert_mgImage_to_array_DT = function(mg_object) {
if(inherits(mg_object, 'giottoImage')) {
mg_object = mg_object@mg_object
}
# data.table variables
RGB = c.1 = c.2 = c.3 = NULL
# convert magick object to an array
num_res = as.numeric(mg_object[[1]])
num_res_m = data.table::as.data.table(reshape2::melt(num_res))
colnames(num_res_m) = c('x', 'y', 'c', 'color')
array_dt = data.table::dcast.data.table(num_res_m, value.var = 'color', formula = 'x+y~c')
colnames(array_dt) = c('x', 'y', 'c.1', 'c.2', 'c.3')
array_dt[, RGB := grDevices::rgb(c.1, c.2, c.3)]
return(array_dt)
}
#' @title estimateImageBg
#' @name estimateImageBg
#' @description helps to estimate which color is the background color of your plot
#' @param mg_object magick image or Giotto image object
#' @param top_color_range top possible background colors to return
#' @return vector of pixel color frequencies and an associated barplot
#' @export
estimateImageBg = function(mg_object, top_color_range = 1:50) {
if(inherits(mg_object, 'giottoImage')) {
mg_object = mg_object@mg_object
}
arrayDT = convert_mgImage_to_array_DT(mg_object = mg_object)
sort_table = sort(table(arrayDT$RGB), decreasing = T)
graphics::barplot(sort_table[top_color_range], col=names(sort_table[top_color_range]))
cat('Most abundant pixel colors: \n')
print(sort_table[top_color_range])
}
#' @title changeImageBg
#' @name changeImageBg
#' @description Function to change the background color of a magick image plot to another color
#' @param mg_object magick image or giotto image object
#' @param bg_color estimated current background color
#' @param perc_range range around estimated background color to include (percentage)
#' @param new_color new background color
#' @param new_name change name of Giotto image
#' @return magick image or giotto image object with updated background color
#' @export
changeImageBg = function(mg_object,
bg_color,
perc_range = 10,
new_color = '#FFFFFF',
new_name = NULL) {
if(inherits(mg_object, 'giottoImage')) {
is_g_image = TRUE
g_image = mg_object
mg_object = mg_object@mg_object
} else {
is_g_image = FALSE
}
if(!inherits(mg_object, 'magick-image')) {
stop("mg_object needs to be a giottImage or a 'magick-image' object from the magick package")
}
# new background color
new_rbg_color = grDevices::col2rgb(new_color)/255
# current background limits
rbgcolors = grDevices::col2rgb(bg_color)/255
perc_range_min = rbgcolors - (rbgcolors/100)*perc_range
perc_range_max = rbgcolors + (rbgcolors/100)*perc_range
# convert magick image to array data.table
arrayDT = convert_mgImage_to_array_DT(mg_object = mg_object)
# create new background
c1_min = perc_range_min[1,1]
c2_min = perc_range_min[2,1]
c3_min = perc_range_min[3,1]
c1_max = perc_range_max[1,1]
c2_max = perc_range_max[2,1]
c3_max = perc_range_max[3,1]
c1_new = new_rbg_color[1,1]
c2_new = new_rbg_color[2,1]
c3_new = new_rbg_color[3,1]
# find background color pixels
# data.table variables
c.1 = c.2 = c.3 = NULL
c1_ind = arrayDT[['c.1']] > c1_min & arrayDT[['c.1']] < c1_max
c2_ind = arrayDT[['c.2']] > c2_min & arrayDT[['c.2']] < c2_max
c3_ind = arrayDT[['c.3']] > c3_min & arrayDT[['c.3']] < c3_max
c_ind = c1_ind*c2_ind*c3_ind
# data.table variables
c1 = c2 = c3 = NULL
# replace old background with new background
arrayDT[, 'c1' := ifelse(c_ind == T, c1_new, c.1)]
arrayDT[, 'c2' := ifelse(c_ind == T, c2_new, c.2)]
arrayDT[, 'c3' := ifelse(c_ind == T, c3_new, c.3)]
# data.table variables
x = y = NULL
# setorder for x and y coordinates
data.table::setorder(arrayDT, y, x)
# convert array_dt to array and then to magick image object
original_width = magick::image_info(mg_object)[2]
original_heigth = magick::image_info(mg_object)[3]
myarray = array(as.vector(as.matrix(arrayDT[,.(c1, c2, c3)])), dim = c(original_width, original_heigth, 3))
new_mg_object = magick::image_read(myarray)
# return magick or giotto image object
if(is_g_image == TRUE) {
if(!is.null(new_name)) g_image$name = new_name
g_image@mg_object = new_mg_object
return(g_image)
} else {
return(new_mg_object)
}
}
#' @title createGiottoImageOLD
#' @name createGiottoImageOLD
#' @description Creates a giotto image that can be added to a Giotto object and/or
#' used to add an image to the spatial plotting functions. Deprecated. See \code{\link{createGiottoImage}}
#' @param gobject giotto object
#' @param spatial_locs spatial locations (alternative if \code{gobject = NULL})
#' @param mg_object \emph{magick} image object
#' @param name name for the image
#' @param xmax_adj adjustment of the maximum x-value to align the image
#' @param xmin_adj adjustment of the minimum x-value to align the image
#' @param ymax_adj adjustment of the maximum y-value to align the image
#' @param ymin_adj adjustment of the minimum y-value to align the image
#' @return a giotto image object
#' @export
createGiottoImageOLD = function(gobject = NULL,
spatial_locs = NULL,
mg_object,
name = 'image',
xmax_adj = 0,
xmin_adj = 0,
ymax_adj = 0,
ymin_adj = 0) {
if(!inherits(mg_object, 'magick-image')) {
if(file.exists(mg_object)) {
mg_object = try(magick::image_read(mg_object))
if(inherits(mg_object, 'try-error')) {
stop(mg_object, ' can not be read by magick::image_read() \n')
}
} else {
stop("mg_object needs to be an image object 'magick-image' from the magick package or \n
an existing path that can be read by magick::image_read()")
}
}
# min and max
if(!is.null(gobject)) {
spatlocs = gobject@spatial_locs
} else if(!is.null(spatial_locs)) {
spatlocs = spatial_locs
} else {
stop('gobject or spatial locations need to be provided')
}
my_xmin = min(spatlocs$sdimx)
my_xmax = max(spatlocs$sdimx)
my_ymin = min(spatlocs$sdimy)
my_ymax = max(spatlocs$sdimy)
# image object
imageObj = list(name = name,
mg_object = mg_object,
minmax = c('xmax_sloc' = my_xmax, 'xmin_sloc' = my_xmin,
'ymax_sloc' = my_ymax, 'ymin_sloc' = my_ymin),
boundaries = c('xmax_adj' = xmax_adj, 'xmin_adj' = xmin_adj,
'ymax_adj' = ymax_adj, 'ymin_adj' = ymin_adj))
class(imageObj) <- append(class(imageObj), 'imageGiottoObj')
return(imageObj)
}
# TODO Check if this is still the best way to do things
#' @name get_img_minmax
#' @title get_img_minmax
#' @param mg_img magick object
#' @param negative_y Map image to negative y spatial values if TRUE during automatic alignment. Meaning that origin is in upper left instead of lower left.
#' @keywords internal
get_img_minmax = function(mg_img,
negative_y = TRUE) {
#Get magick object dimensions. xmin and ymax assumed to be 0.
info = magick::image_info(mg_img)
img_xmax = info$width #width
img_xmin = 0 #x origin
if(negative_y == TRUE) {
img_ymax = 0 #y origin
img_ymin = -(info$height) #height
} else if(negative_y == FALSE) {
img_ymax = info$height
img_ymin = 0
}
return(list('img_xmax' = img_xmax,
'img_xmin' = img_xmin,
'img_ymax' = img_ymax,
'img_ymin' = img_ymin))
}
# TODO Check if this is still the best way to do things
#' @name get_adj_rescale_img
#' @title get_adj_rescale_img
#' @keywords internal
get_adj_rescale_img = function(img_minmax,
spatial_locs,
scale_factor = 1) {
# Expand scale_factor if needed
if(length(scale_factor) == 1) {
scale_factor = c(x = scale_factor, y = scale_factor)
}
# Spatial minmax
my_xmin = min(spatial_locs$sdimx)
my_xmax = max(spatial_locs$sdimx)
my_ymin = min(spatial_locs$sdimy)
my_ymax = max(spatial_locs$sdimy)
# Find scaled image adjustments based on scaled spatlocs
xmin_adj_scaled = (my_xmin*scale_factor[['x']]) - (img_minmax$img_xmin)
xmin_adj_orig = xmin_adj_scaled/scale_factor[['x']]
xmax_adj_scaled = (img_minmax$img_xmax) - (my_xmax*scale_factor[['x']])
xmax_adj_orig = xmax_adj_scaled/scale_factor[['x']]
ymin_adj_scaled = (my_ymin*scale_factor[['y']]) - (img_minmax$img_ymin)
ymin_adj_orig = ymin_adj_scaled/scale_factor[['y']]
ymax_adj_scaled = (img_minmax$img_ymax) - (my_ymax*scale_factor[['y']])
ymax_adj_orig = ymax_adj_scaled/scale_factor[['y']]
# return scaled adjustments
return(c('xmin_adj_orig' = xmin_adj_orig,
'xmax_adj_orig' = xmax_adj_orig,
'ymin_adj_orig' = ymin_adj_orig,
'ymax_adj_orig' = ymax_adj_orig))
}
#' @title addGiottoImageMG
#' @name addGiottoImageMG
#' @description Adds giotto image objects to your giotto object
#' @param gobject giotto object
#' @param images list of giotto image objects, see \code{\link{createGiottoImage}}
#' @param spat_unit spatial unit
#' @param spat_loc_name provide spatial location slot in Giotto to align images. Defaults to first one
#' @param scale_factor provide scale of image pixel dimensions relative to spatial coordinates.
#' @param negative_y Map image to negative y spatial values if TRUE during automatic alignment. Meaning that origin is in upper left instead of lower left.
#' @return an updated Giotto object with access to the list of images
#' @export
addGiottoImageMG = function(gobject,
images,
spat_unit = NULL,
spat_loc_name = NULL,
scale_factor = NULL,
negative_y = TRUE) {
# 0. check params
if(is.null(gobject)) stop('The giotto object that will be updated needs to be provided')
if(is.null(images)) stop('The giotto image(s) that will be added needs to be provided')
if(is.null(spat_loc_name)) {
if(!is.null(slot(gobject, 'spatial_locs'))) {
spat_loc_name = list_spatial_locations(gobject = gobject, spat_unit = spat_unit)[1,]
} else {
spat_loc_name = NULL
cat('No spatial locations have been found \n')
}
}
ext_scale_factor = FALSE
if(!is.null(scale_factor)) {
if(!is.numeric(scale_factor)) stop ('Given scale_factor(s) must be numeric')
if((length(scale_factor) == length(images)) || length(scale_factor) == 1) {
cat('scale_factor(s) external to giottoImage have been given and will be used')
ext_scale_factor = TRUE
} else {
stop('if scale_factor is given, it must be a numeric with either a single value or as many values as there are images are provided')
}
}
# 1. expand scale_factors
if(ext_scale_factor == TRUE) {
if(length(scale_factor == 1)) {
scale_factor = rep(scale_factor, length(images))
}
}
# 2. Add image with for loop
for(image_i in 1:length(images)) {
im = images[[image_i]]
if(inherits(im, 'giottoImage')) {
im_name = im@name
all_im_names = names(gobject@images)
if(im_name %in% all_im_names) {
cat('\n ', im_name, ' has already been used, will be overwritten \n')
}
# 3. Update boundaries if not already done during createGiottoImage() due to lack of spatlocs and gobject
if(sum(im@boundaries == c(0,0,0,0)) == 4 && sum(im@minmax == c(10,0,10,0)) == 4) {
if(!is.null(spat_loc_name)) { # A check for the first available spatloc was already done
spatlocs = get_spatial_locations(gobject = gobject,
spat_unit = spat_unit,
spat_loc_name = spat_loc_name)
#Find spatial minmax values
xmin_sloc = min(spatlocs$sdimx)
xmax_sloc = max(spatlocs$sdimx)
ymin_sloc = min(spatlocs$sdimy)
ymax_sloc = max(spatlocs$sdimy)
#Find adjustment values
img_minmax = get_img_minmax(mg_img = im@mg_object,
negative_y = negative_y)
if(ext_scale_factor == TRUE) {
adj_values = get_adj_rescale_img(img_minmax = img_minmax,
spatial_locs = spatlocs,
scale_factor = scale_factor[[image_i]])
} else if (ext_scale_factor == FALSE) {
adj_values = get_adj_rescale_img(img_minmax = img_minmax,
spatial_locs = spatlocs,
scale_factor = im@scale_factor[[spat_loc_name]])
}
#Add minmax values to giottoImage@minmax
im@minmax = c('xmax_sloc' = xmax_sloc,
'xmin_sloc' = xmin_sloc,
'ymax_sloc' = ymax_sloc,
'ymin_sloc' = ymin_sloc)
#Add adjustment values to giottoImage@boundaries
im@boundaries = c('xmax_adj' = as.numeric(adj_values[['xmax_adj_orig']]),
'xmin_adj' = as.numeric(adj_values[['xmin_adj_orig']]),
'ymax_adj' = as.numeric(adj_values[['ymax_adj_orig']]),
'ymin_adj' = as.numeric(adj_values[['ymin_adj_orig']]))
# Inherit external scaling factors if given
if(ext_scale_factor == TRUE) {
im@scale_factor[[spat_loc_name]] = scale_factor[[image_i]]
im@resolution[[spat_loc_name]] = 1/(scale_factor[[image_i]])
}
## Externally given scale_factors will only be written in/used if boundary adj values are not pre-existing
}
}
# 4. Add giottoImage to gobject
gobject@images[[im_name]] = im
} else {
warning('image [',image_i,'] is not a giotto image object')
}
}
return(gobject)
}
#' @title addGiottoImageToSpatPlot
#' @name addGiottoImageToSpatPlot
#' @description Add a giotto image to a spatial ggplot object post creation
#' @param spatpl a spatial ggplot object
#' @param gimage a giotto image, see \code{\link{createGiottoImage}}
#' @param layer numeric layer on which to add the giotto image. OR takes 'bg' or
#' 'overlay' as input to designate last (bottom/background) or first (top/overlay)
#' @param alpha (optional) add giotto image to plot with transparency. Numeric. From 0
#' (transparent) to 1 (fully visible)
#' @return an updated spatial ggplot object
#' @export
addGiottoImageToSpatPlot = function(spatpl = NULL,
gimage = NULL,
layer = c('bg', 'overlay'),
alpha = NULL) {
layer = match.arg(arg = layer, choices = c('bg', 'overlay'))
if(is.null(spatpl) | is.null(gimage)) {
stop('A spatial ggplot object and a giotto image need to be given')
}
# extract min and max from object
my_xmax = gimage@minmax[1]
my_xmin = gimage@minmax[2]
my_ymax = gimage@minmax[3]
my_ymin = gimage@minmax[4]
# convert giotto image object into array
img_array = as.numeric(gimage@mg_object[[1]])
# add transparency if needed
if(!is.null(alpha) & is.numeric(alpha)) {
img_array = add_img_array_alpha(x = img_array,
alpha = alpha)
}
# extract adjustments from object
xmax_b = gimage@boundaries[1]
xmin_b = gimage@boundaries[2]
ymax_b = gimage@boundaries[3]
ymin_b = gimage@boundaries[4]
newpl = spatpl + annotation_raster(img_array,
xmin = my_xmin-xmin_b, xmax = my_xmax+xmax_b,
ymin = my_ymin-ymin_b, ymax = my_ymax+ymax_b)
# position new layer
if(layer == 'bg') {
# move image to background
nr_layers = length(newpl$layers)
newpl$layers = c(newpl$layers[[nr_layers]], newpl$layers[1:(nr_layers-1)])
} else if(layer == 'overlay') {} # keep image on top
return(newpl)
}
#' @title updateGiottoImageMG
#' @name updateGiottoImageMG
#' @description Updates the boundaries of a giotto \code{image} object attached to
#' a \code{giotto} object if both \code{gobject} and \code{image_name} params
#' are given. Alternatively can directly accept and return as \code{image}
#' @inheritParams updateGiottoImage
#' @param gobject \code{giotto} object containing giotto \code{image} object
#' @param giottoImage \code{image} object to directly update
#' @param xmin_set set image xmin boundary. Applied before adjustments
#' @param xmax_set set image xmax boundary. Applied before adjustments
#' @param ymin_set set image ymin boundary. Applied before adjustments
#' @param ymax_set set image ymax boundary. Applied before adjustments
#' @param return_gobject return a \code{giotto} object if \code{TRUE}, a giotto
#' \code{image} object if \code{FALSE}
#' @return a \code{giotto} object or an updated giotto \code{image} object if
#' \code{return_gobject = FALSe}
#' @export
updateGiottoImageMG = function(gobject = NULL,
image_name = NULL,
giottoImage = NULL,
xmax_adj = 0,
xmin_adj = 0,
ymax_adj = 0,
ymin_adj = 0,
x_shift = 0,
y_shift = 0,
scale_factor = NULL,
scale_x = 1,
scale_y = 1,
order = c('first_adj','first_scale'),
xmin_set = NULL,
xmax_set = NULL,
ymin_set = NULL,
ymax_set = NULL,
return_gobject = TRUE,
verbose = TRUE) {
# 0. Check params
# Check input image
if(is.null(gobject)) {
if(is.null(giottoImage)) stop('Image to be updated must be given as gobject AND image_name OR giottoImage argument(s) \n')
if(verbose == TRUE) cat('gobject argument not given \n return_gobject set to FALSE \n')
return_gobject = FALSE
}
if(is.null(giottoImage) && is.null(image_name)) stop('The name of the giotto image that will be updated needs to be provided \n')
if(!is.null(giottoImage)) {
if(!inherits(giottoImage, 'giottoImage')) stop('giottoImage argument only accepts giottoImage objects \n')
if(verbose == TRUE && !is.null(gobject)) cat('giottoImage argument is given and will take priority \n return_gobject set to FALSE \n')
return_gobject = FALSE
}
# Check scalefactors
if(!is.null(scale_factor)) scale_x = scale_y = scale_factor
# Check spatial anchor values
spatAnchor = c('xmax_sloc' = xmax_set,
'xmin_sloc' = xmin_set,
'ymax_sloc' = ymax_set,
'ymin_sloc' = ymin_set)
if(length(spatAnchor) < 4 && length(spatAnchor) > 0) stop('If set arguments are being used, all four must be given \n')
if(!is.null(spatAnchor)) {
if(xmax_set < xmin_set) stop('xmax_set must be greater than xmin_set \n')
if(ymax_set < ymin_set) stop('ymax_set must be greater than ymin_set \n')
}
# Find order of adjust and scaling
order = match.arg(order, choices = c('first_adj','first_scale'))
# 1. get giottoImage if necessary
if(is.null(giottoImage)) {
if(!is.null(gobject) && !is.null(image_name)) {
g_image = get_giottoImage_MG(gobject = gobject,
name = image_name)
} else {
stop('either a giottoImage or both the gobject and name of the giottoImage must be given. \n')
}
}
# 2. Find minmax spatial anchor values
if(is.null(spatAnchor)) {
spatAnchor = g_image@minmax
}
# Perform scale if first_scale
if(order == 'first_scale') {
spatAnchor = spatAnchor * c(scale_x, scale_x, scale_y, scale_y)
}
# 3. Prepare adjustment values
# Apply shifts
xmin_adj = xmin_adj - x_shift
xmax_adj = xmax_adj + x_shift
ymin_adj = ymin_adj - y_shift
ymax_adj = ymax_adj + y_shift
# Find final bounds
xmax_final = spatAnchor[['xmax_sloc']] + xmax_adj
xmin_final = spatAnchor[['xmin_sloc']] - xmin_adj
ymax_final = spatAnchor[['ymax_sloc']] + ymax_adj
ymin_final = spatAnchor[['ymin_sloc']] - ymin_adj
# Perform scale if first_adj
if(order == 'first_adj') {
xmax_final = xmax_final * scale_x
xmin_final = xmin_final * scale_x
ymax_final = ymax_final * scale_y
ymin_final = ymin_final * scale_y
}
# Find final adj values
xmax_adj = xmax_final - g_image@minmax[['xmax_sloc']]
xmin_adj = g_image@minmax[['xmin_sloc']] - xmin_final
ymax_adj = ymax_final - g_image@minmax[['ymax_sloc']]
ymin_adj = g_image@minmax[['ymin_sloc']] - ymin_final
# 4. Update the boundaries
g_image@boundaries = c('xmax_adj' = xmax_adj,
'xmin_adj' = xmin_adj,
'ymax_adj' = ymax_adj,
'ymin_adj' = ymin_adj)
# 5. Update the scalefactors for x and y
x_range = xmax_final - xmin_final
y_range = ymax_final - ymin_final
im_dims = magick::image_info(g_image@mg_object)
x_scalefactor = im_dims[['width']] / x_range
y_scalefactor = im_dims[['height']] / y_range
g_image@scale_factor = c('x' = x_scalefactor, 'y' = y_scalefactor)
g_image@resolution = (1/g_image@scale_factor)
if(return_gobject == TRUE) {
gobject@images[[image_name]] = g_image
return(gobject)
} else {
return(g_image)
}
}
#' @title plot_giottoImage_MG
#' @name plot_giottoImage_MG
#' @description get and plot a giottoImage either directly or from a giotto object
#' @param gobject giotto object
#' @param image_name name of giotto image \code{\link{showGiottoImageNames}}
#' @param giottoImage giottoImage object
#' @return plot
#' @keywords internal
plot_giottoImage_MG = function(gobject = NULL,
image_name = NULL,
giottoImage = NULL) {
if(!is.null(giottoImage)) {
graphics::plot(giottoImage@mg_object)
} else {
if(is.null(gobject)) stop('The giotto object that will be updated needs to be provided \n')
if(is.null(image_name)) stop('The name of the giotto image that will be updated needs to be provided \n')
g_image_names = names(gobject@images)
if(!image_name %in% g_image_names) stop(image_name, ' was not found among the image names, see showImageNames()')
graphics::plot(gobject@images[[image_name]]@mg_object)
}
}
#' @title reconnect_giottoImage_MG
#' @name reconnect_giottoImage_MG
#' @description reconnect giottoImage if image pointer is broken
#' @param giottoImage giottoImage to reconnect
#' @param image_path path to source file of giottoImage
#' @return reconnected giottoImage
#' @keywords internal
reconnect_giottoImage_MG = function(giottoImage,
image_path) {
# load in new magick object
mg_object = magick::image_read(image_path)
# replace old magick object
giottoImage@mg_object = mg_object
# return reconnected giottoImage
return(giottoImage)
}
# giottoLargeImage or terra tools ####
#' @title Load image as a terra spatRaster object
#' @name create_terra_spatRaster
#' @param image_path existing full filepath to image to be loaded as a terra spatRaster
#' @keywords internal
create_terra_spatRaster = function(image_path) {
raster_object = try(suppressWarnings(terra::rast(x = image_path)))
if(inherits(raster_object, 'try-error')) {
stop(raster_object, ' can not be read by terra::rast() \n')
}
return(raster_object)
}
#' @title Plot smoothed curve of giotto largeImage intensity values
#' @name density_giottoLargeImage
#' @param gobject giotto object
#' @param image_type image object type (only supports largeImage and is set as
#' default)
#' @param giottoLargeImage giotto large image object
#' @param method method of plotting image distribution
#' @keywords internal
dist_giottoLargeImage = function(gobject = NULL,
image_name = NULL,
giottoLargeImage = NULL,
method = 'dens') {
# get image object
if(!is.null(gobject) & !is.null(image_name)) {
img_obj = get_giottoImage(gobject = gobject,
image_type = 'largeImage',
name = image_name)
} else if(!is.null(giottoLargeImage)){
img_obj = giottoLargeImage@raster_object
} else {
stop('No giottoLargeImage given \n')
}
# plot curve
if(method == 'dens') terra::density(img_obj)
if(method == 'hist') terra::hist(img_obj)
}
#' @title Stitch multiple giottoLargeImage objects into a single giottoLargeImage object
#' @name stitchGiottoLargeImage
#' @description Function to stitch together multiple field of view (FOV) images into a
#' single final image. Images are loaded into Giotto as \code{giottoLargeImage} and
#' stitched based on a set of FOV positions into a single final \code{giottoLargeImage}.
#' @details This function is time consuming. Setting a save location through the
#' \code{filename} parameter is also highly recommended as file size will likely be large.
#' This function creates a single stitched image from multiple FOV tiles and saves that
#' image to disk as it works. When finished, the pointer to that new image is loaded in
#' as a \code{giottoLargeImage} object. \cr
#' \strong{Note:} Dry runs are on by default and \code{dryRun} param must be set to FALSE
#' to proceed with the final stitching operation.
#' @section Dry runs are default:
#' To ensure that disk space and time is not wasted, this function defaults to previewing
#' the stitching operation.
#' @section FOV positions:
#' The final image is stitched together from multiple FOV tiles. The \code{FOV_positions}
#' parameter accepts a table of x and y values for where each FOV tile should be placed
#' when performing the stitch. Which columns are the x and y values are determined by
#' the \code{FOV_xcol} and \code{FOV_ycol} params respectively. FOV tiles are at full resolution
#' with a starting position where either the lower left or upper left of the image touch the origin
#' depending on the value of \code{FOV_inverty} param. The FOV image is then translated according
#' to the x and y shift values.
#' @section FOV invert y:
#' Many imaging systems may treat the origin according to image convention where (0,0) is at the
#' upper left of an image. This is at odds with coordinate convention and what Giotto uses internally
#' where the coordinate (0,0) is at the lower left. The \code{FOV_inverty} defaults to FALSE, but if
#' set to TRUE, then FOV tile images will start with the upper left touching (0,0) and all y values
#' given through \code{FOV_positions} and \code{FOV_ycol} will be treated as negative y shift values.
#' @section dataType:
#' There are multiple datatypes defining the range of intensity values that images can be saved
#' with. Setting a value with the \code{dataType} para is optional and Giotto attempts
#' to determine compatible data type to save the image as automatically.
#' @param largeImage_list list of \code{giottoLargeImage} objects
#' @param gobject_list list of \code{gobjects} containing \code{giottoLargeImages}
#' @param largeImage_nameList list of names of \code{giottoLargeImages} within \code{gobjects}
#' @param FOV_positions dataframe of FOV positions. Values (if any) are directly added to current image mapping
#' @param FOV_xcol column name for FOV position x values
#' @param FOV_ycol column name for FOV position y values
#' @param FOV_inverty make FOV y position values negative
#' @param method method of stitching images (\strong{mosaic}: average overlapping area values,
#' \strong{merge}:values get priority by order of images given)
#' @param round_positions [boolean] round image positions. May be necessary to run.
#' @param filename file name to write the stitched image to. Defaults to \code{"save_dir/stitch.tif"}
#' if \code{save_dir} param is found in the first \code{gobject}'s Giotto instructions
#' @param dataType (optional) values for \code{dataType} are "INT1U", "INT2U", "INT2S", "INT4U",
#' "INT4S", "FLT4S", "FLT8S". The first three letters indicate whether the \code{dataType} is
#' integer (whole numbers) of a real number (decimal numbers), the fourth character indicates
#' the number of bytes used (allowing for large numbers and/or more precision), and the "S" or "U"
#' indicate whether the values are signed (both negative and positive) or unsigned (positive values only).
#' @param fileType (optional) image format (e.g. .tif) If not given, defaults to format given in the filename
#' @param dryRun [boolean] plot placeholder bounding rectangles where FOV images will be stitched without
#' actually proceeding with the full image stitching and saving process.
#' @param overwrite [boolean] overwrite if filename to save image as already exists. Defaults to TRUE
#' @param verbose [boolean] be verbose
#' @return \code{largeGiottoImage} object with pointer to stitched image
#' @export
stitchGiottoLargeImage = function(largeImage_list = NULL,
gobject_list = NULL,
largeImage_nameList = NULL,
FOV_positions = NULL,
FOV_xcol = NULL,
FOV_ycol = NULL,
FOV_inverty = FALSE,
method = c('mosaic','merge'),
round_positions = FALSE,
filename = NULL,
dataType = NULL,
fileType = NULL,
dryRun = TRUE,
overwrite = FALSE,
verbose = TRUE) {
## 0. Check params
if(!is.null(gobject_list)) {
# Set default largeImage_nameList
if(is.null(largeImage_nameList)) {
largeImage_nameList = rep("image", length(gobject_list))
}
}
# Select method for stitching
method = match.arg(method, choices = c('mosaic', 'merge'))
# Check for filename, set default if not found
if(is.null(filename)) {
if(!is.null(gobject_list)) {
save_dir = readGiottoInstructions(gobject_list[[1]], param = 'save_dir')
} else {
save_dir = path.expand('~')
}
filename = paste0(save_dir, '/stitch.tif')
}
# check filename
if(file.exists(filename)) {
if(verbose == TRUE) {
if(overwrite == TRUE) cat('File at',filename,'exists.\n (overwrite == TRUE) Image will be overwritten')
if(overwrite == FALSE) cat('File at',filename,'exists.\n (overwrite == FALSE) Image will not be overwritten')
}
}
# Match dataType input if given
dataTypeChoices = c('INT1U','INT2U','INT2S','INT4U','INT4S','FLT4S','FLT8S')
if(!is.null(dataType)) dataType = match.arg(dataType, choices = dataTypeChoices)
# Determine compatible dataType from first giottoLargeImage
## 1. Get list of raster objects
if(is.null(largeImage_list)) {
if(!is.null(gobject_list)) {
# For loop to grab giottoLargeImages
largeImage_list = list()
for(gobj_i in 1:length(gobject_list)) {
largeImage_list[[gobj_i]] = get_giottoLargeImage(gobject = gobject_list[[gobj_i]],
name = largeImage_nameList[[gobj_i]])
}
} else {
stop('giottoLargeImages must be given either as the giottoLargeImage itself or as a giotto object and the giottoLargeImage name')
}
}
# Determine datatype from first giottoLargeImage
if(is.null(dataType)) {
dataType = find_terra_writeRaster_dataType(giottoLargeImage = largeImage_list[[1]])
}
# For loop to extract raster_objects
raster_list = list()
for(img_i in 1:length(largeImage_list)) {
raster_list[[img_i]] = largeImage_list[[img_i]]@raster_object
}
## 2. Apply FOV shifts (if given)
if(!is.null(FOV_positions)) {
# Check if there is an FOV position for every raster object
if(nrow(FOV_positions) != length(raster_list)) {
stop('If FOV_positions are given then there must be one set of values for every image being stitched')
}
if(FOV_inverty == TRUE) {
FOV_positions['FOV_ycol'] = -FOV_positions['FOV_ycol']
}
# Shift the image extents as specified by POV_positions
for(rast_i in 1:length(raster_list)) {
raster_list[[rast_i]] = terra::shift(x = raster_list[[rast_i]],
dx = FOV_positions[rast_i,FOV_xcol],
dy = FOV_positions[rast_i,FOV_ycol])
}
}
## 3. Perform stitch
# # Round final extent values (merge and mosaic may only work with integer extents)
if(round_positions == TRUE) {
if(verbose == TRUE) cat('round_positions == TRUE \n Image spatial positions will be rounded to integers. \n')
for(rast_i in 1:length(raster_list)) {
terra::ext(raster_list[[rast_i]]) = round(terra::ext(raster_list[[rast_i]]))
}
}
if(dryRun == TRUE) {
# Collect SpatExtents then convert to polygons
imgBounds_list = list()
for(rast_i in 1:length(raster_list)) {
img_ext = terra::ext(raster_list[[rast_i]])
img_bound_poly = terra::as.polygons(img_ext)
img_bound_poly$FOV = rast_i
# Add to imgBounds list
imgBounds_list[[rast_i]] = img_bound_poly
}
imgBounds = do.call(rbind, imgBounds_list)
terra::plot(imgBounds, 'FOV',
type = 'classes',
legend = TRUE,
mar = c(3,3,2,2),
plg = list(x = 'topright'))
return(NULL)
} else if (dryRun == FALSE) {
# Create SpatRasterCollection
rasterSRC = terra::sprc(raster_list)
# stitch raster objects
if(method == 'merge') {
stitchImg = terra::merge(x = rasterSRC,
filename = filename,
overwrite = overwrite,
wopt = list(datatype = dataType))
} else if(method == 'mosaic') {
stitchImg = terra::mosaic(x = rasterSRC,
filename = filename,
overwrite = overwrite,
wopt = list(datatype = dataType,
filetype = fileType))
}
stitch_gLargeImg = createGiottoLargeImage(raster_object = stitchImg,
use_rast_ext = TRUE)
return(stitch_gLargeImg)
}
}
#' @title Crop a giotto largeImage object
#' @name cropGiottoLargeImage
#' @description Crop a giottoLargeImage based on crop_extent argument or given values
#' @param gobject gobject holding the giottoLargeImage
#' @param largeImage_name name of giottoLargeImage within gobject
#' @param giottoLargeImage alternative input param using giottoLargeImage object
#' instead of through \code{gobject} and \code{largeImage_name} params
#' @param crop_name arbitrary name for cropped giottoLargeImage
#' @param crop_extent terra extent object used to crop the giottoLargeImage
#' @param xmax_crop,xmin_crop,ymax_crop,ymin_crop crop min/max x and y bounds
#' @return a giottoLargeImage object
#' @export
cropGiottoLargeImage = function(gobject = NULL,
largeImage_name = NULL,
giottoLargeImage = NULL,
crop_name = 'image',
crop_extent = NULL,
xmax_crop = NULL,
xmin_crop = NULL,
ymax_crop = NULL,
ymin_crop = NULL) {
## 0. Check inputs
if(!is.null(crop_extent)) {
if(!inherits(crop_extent, 'SpatExtent')) stop('crop_extent argument only accepts terra extent objects. \n')
}
if(!is.null(giottoLargeImage)) {
if(!inherits(giottoLargeImage, 'giottoLargeImage')) stop('giottoLargeImage argument only accepts giottoLargeImage objects. \n')
}
## 1. get giottoLargeImage if necessary
if(is.null(giottoLargeImage)) {
if(!is.null(gobject) && !is.null(largeImage_name)) {
giottoLargeImage = get_giottoLargeImage(gobject = gobject,
name = largeImage_name)
} else {
stop('either a giottoLargeImage or both the gobject and name of the giottoLargeImage must be given. \n')
}
}
raster_object = giottoLargeImage@raster_object
## 2. Find crop extent
crop_bounds = c(xmin_crop,xmax_crop,ymin_crop,ymax_crop)
if(!is.null(crop_extent)) {
raster_object = terra::crop(raster_object,
crop_extent,
snap = 'near')
} else if(length(crop_bounds == 4)) {
crop_extent = terra::ext(crop_bounds)
raster_object = terra::crop(raster_object,
crop_extent,
snap = 'near')
} else if(length(crop_bounds) > 1) {
stop('All four crop bounds must be given.')
}
## 3. Return a cropped giottoLargeImage
giottoLargeImage@name = crop_name
giottoLargeImage@raster_object = raster_object
giottoLargeImage@extent = as.vector(terra::ext(raster_object))
# The only things updated are the raster object itself, the name, and the extent tracking slot.
# The overall_extent slot must NOT be updated since it records the original extent
return(giottoLargeImage)
}
#TODO link this up to plot_auto_largeImage_resample() ?
#' @title plot_giottoLargeImage
#' @name plot_giottoLargeImage
#' @description Plot a \emph{downsampled} version of giottoLargeImage. Cropping can increase plot resolution of region of interest.
#' @param gobject giotto object
#' @param largeImage_name name of giottoLargeImage
#' @param giottoLargeImage giottoLargeImage object
#' @param crop_extent (optional) extent object to focus on specific region of image
#' @param xmax_crop,xmin_crop,ymax_crop,ymin_crop (optional) crop min/max x and y bounds
#' @param max_intensity (optional) value to treat as maximum intensity in color scale
#' @param asRGB (optional) [boolean] force RGB plotting if not automatically detected
#' @param stretch character. Option to stretch the values to increase contrast: "lin"
#' linear or "hist" (histogram)
#' @return plot
#' @keywords internal
plot_giottoLargeImage = function(gobject = NULL,
largeImage_name = NULL,
giottoLargeImage = NULL,
crop_extent = NULL,
xmax_crop = NULL,
xmin_crop = NULL,
ymax_crop = NULL,
ymin_crop = NULL,
max_intensity = NULL,
asRGB = FALSE,
stretch = NULL) {
# Get giottoLargeImage and check and perform crop if needed
giottoLargeImage = cropGiottoLargeImage(gobject = gobject,
largeImage_name = largeImage_name,
giottoLargeImage = giottoLargeImage,
crop_extent = crop_extent,
xmax_crop = xmax_crop,
xmin_crop = xmin_crop,
ymax_crop = ymax_crop,
ymin_crop = ymin_crop)
raster_object = giottoLargeImage@raster_object
# plot
if(isTRUE(asRGB) | terra::has.RGB(raster_object) | terra::nlyr(raster_object) == 3) {
# Determine likely image bitdepth
if(is.null(max_intensity)) {
bitDepth = ceiling(log(x = giottoLargeImage@max_intensity, base = 2))
# Assign discovered bitdepth as max_intensity
max_intensity = 2^bitDepth-1
}
terra::plotRGB(raster_object,
axes = TRUE,
r = 1,g = 2,b = 3,
scale = max_intensity,
stretch = stretch,
smooth = TRUE,
mar = c(3,5,1.5,1),
asp = 1)
} else {
if(is.null(stretch)) stretch = 'lin'
terra::plotRGB(raster_object,
axes = TRUE,
r = 1,g = 1,b = 1,
stretch = stretch,
smooth = TRUE,
mar = c(3,5,1.5,1),
asp = 1)
}
}
#' @title convertGiottoLargeImageToMG
#' @name convertGiottoLargeImageToMG
#' @description convert a giottoLargeImage by downsampling into a normal magick based giottoImage
#' @param gobject gobject containing giottoLargeImage
#' @param largeImage_name name of giottoLargeImage
#' @param giottoLargeImage alternative input param using giottoLargeImage object
#' instead of through \code{gobject} and \code{largeImage_name} params
#' @param mg_name name to assign converted magick image based giottoImage. Defaults to name of giottoLargeImage
#' @param spat_unit spatial unit
#' @param spat_loc_name gobject spatial location name to map giottoImage to (optional)
#' @param crop_extent extent object to focus on specific region of image
#' @param xmax_crop assign crop boundary
#' @param xmin_crop assign crop boundary
#' @param ymax_crop assign crop boundary
#' @param ymin_crop assign crop boundary
#' @param resample_size maximum number of pixels to use when resampling
#' @param max_intensity value to treat as maximum intensity in color scale
#' @param return_gobject return as giotto object
#' @param verbose be verbose
#' @return a giotto object if \code{return_gobject = TRUE} or an updated giotto
#' image object if \code{return_gobject = FALSE}
#' @export
convertGiottoLargeImageToMG = function(gobject = NULL,
largeImage_name = NULL,
giottoLargeImage = NULL,
mg_name = NULL,
spat_unit = NULL,
spat_loc_name = NULL,
crop_extent = NULL,
xmax_crop = NULL,
xmin_crop = NULL,
ymax_crop = NULL,
ymin_crop = NULL,
resample_size = 500000,
max_intensity = NULL,
return_gobject = TRUE,
verbose = TRUE) {
# Check params
if(is.null(gobject)) {
if(return_gobject == TRUE) stop('gobject must be given if return_gobject == TRUE \n')
if(!is.null(spat_loc_name)) stop('if spatial location name is given then gobject containing it must also be given \n')
}
# Set spat_unit
spat_unit = set_default_spat_unit(gobject = gobject,
spat_unit = spat_unit)
# Get giottoLargeImage and check and perform crop if needed
giottoLargeImage = cropGiottoLargeImage(gobject = gobject,
largeImage_name = largeImage_name,
giottoLargeImage = giottoLargeImage,
crop_extent = crop_extent,
xmax_crop = xmax_crop,
xmin_crop = xmin_crop,
ymax_crop = ymax_crop,
ymin_crop = ymin_crop)
raster_object = giottoLargeImage@raster_object
# Resample and then convert to Array
rastSample = terra::spatSample(raster_object,
size = resample_size, # Defines the rough maximum of pixels allowed when resampling
method = 'regular',
as.raster = TRUE)
imArray = terra::as.array(rastSample)
# Set max_intensity
if(is.null(max_intensity)) {
max_intensity = max(imArray)
}
# Read in array as magick image
mImg = magick::image_read(imArray/max_intensity)
# Set boundary adj values
xmin_adj = xmax_adj = ymin_adj = ymax_adj = 0
# magick object name
if(is.null(mg_name)) {
mg_name = giottoLargeImage@name
}
# Create giottoImage
g_image = createGiottoImage(name = mg_name,
mg_object = mImg,
do_manual_adj = TRUE,
xmax_adj = xmax_adj,
xmin_adj = xmin_adj,
ymax_adj = ymax_adj,
ymin_adj = ymin_adj,
verbose = FALSE)
# Set minimax
if(is.null(spat_loc_name)) {
current_ext = terra::ext(raster_object)
g_image@minmax = c(current_ext$xmax,
current_ext$xmin,
current_ext$ymax,
current_ext$ymin)
} else if(!is.null(spat_loc_name)) {
spatial_locs = get_spatial_locations(gobject = gobject,
spat_unit = spat_unit,
spat_loc_name = spat_loc_name)
x_range = range(spatial_locs$sdimx)
y_range = range(spatial_locs$sdimy)
g_image@minmax = c(x_range[2],
x_range[1],
y_range[2],
y_range[1])
}
names(g_image@minmax) = c('xmax_sloc','xmin_sloc','ymax_sloc','ymin_sloc')
# Set scalefactor
im_dims = magick::image_info(g_image@mg_object)
x_scalefactor = im_dims[['width']] / dim(raster_object)[2]
y_scalefactor = im_dims[['height']] / dim(raster_object)[1]
g_image@scale_factor = c('x' = x_scalefactor, 'y' = y_scalefactor)
g_image@resolution = 1/g_image@scale_factor
if(return_gobject == TRUE) {
if(verbose == TRUE) {
if(mg_name %in% names(gobject@images)) cat('\n ', mg_name, ' has already been used, will be overwritten \n')
}
gobject@images[[mg_name]] = g_image
return(gobject)
} else if(return_gobject == FALSE) {
return(g_image)
}
}
#' @title find_terra_writeRaster_dataType
#' @name find_terra_writeRaster_dataType
#' @description find likely compatible datatype for given image characteristics.
#' Values given in arguments take priority over those found from giottoLargeImage
#' metadata
#' @param giottoLargeImage giottoLargeImage object to determine max_intensity,
#' min_intensity, is_int settings from
#' @param quick_INTU_maxval Treat as maximum intensity to find compatible unsigned
#' integer settings
#' @param max_intensity,min_intensity value given as image maximum/minimum intensity
#' @param is_int if image is integer (TRUE) or floating point (FALSE)
#' @param signed if image is signed (TRUE) or unsigned (TRUE)
#' @param bitDepth image bitDepth
#' @param verbose be verbose
#' @keywords internal
#' @return datatype for terra writeRaster function
find_terra_writeRaster_dataType = function(giottoLargeImage = NULL,
quick_INTS_maxval = NULL,
max_intensity = NULL,
min_intensity = NULL,
is_int = NULL,
signed = NULL,
bitDepth = NULL,
verbose = TRUE) {
# 1. Get any missing metadata from giottoLargeImage object if given
if(!is.null(giottoLargeImage)) {
if(is.null(max_intensity)) max_intensity = giottoLargeImage@max_intensity
if(is.null(min_intensity)) min_intensity = giottoLargeImage@min_intensity
if(is.null(is_int)) is_int = giottoLargeImage@is_int
}
if(is.null(quick_INTS_maxval)) {
if(length(c(max_intensity, min_intensity)) < 2) stop('Not enough metadata is given')
# Determine if negative values are present
if(min_intensity < 0) {
signed = TRUE
}
}
# Set defaults if data still missing
if(is.null(is_int)) is_int = TRUE
if(is.null(signed)) signed = FALSE
## 2. Determine likely compatible datatype
dataTypeVerbose = data.frame(bitDepth = c(8,16,16,32,32,32,64),
signed = c(FALSE,FALSE,TRUE,FALSE,TRUE,TRUE,TRUE),
integer = c(TRUE,TRUE,TRUE,TRUE,TRUE,FALSE,FALSE),
dataTypeChoices = c('INT1U','INT2U','INT2S','INT4U','INT4S','FLT4S','FLT8S'),
dataTypeVerbose = c('8bit unsigned integer','16bit unsigned integer','16bit signed integer',
'32bit unsigned integer','32bit signed integer','32bit signed floating point',
'64bit signed floating point'))
## Find Compatible Bitdepth
# If quick_INTS_maxval argument is set, will be treated as the highest needed (unsigned preferred) bitdepth datatype
if(is.null(quick_INTS_maxval)) {
if(max_intensity > 0) {
max_intensity = max_intensity + 1 # Accounts for 0 occupying 1 of the available values.
if(signed == FALSE) {
bitDepth = ceiling(log(x = max_intensity, base = 2))
} else if(signed == TRUE) {
intensityMinMax = c(min_intensity, max_intensity)
intensityMinMax = abs(intensityMinMax)
bitDepthMinMax = ceiling(log(x = intensityMinMax, base = 2))
bitDepth = max(bitDepthMinMax) + 1
}
} else {
stop('There are no positive image intensities. \n Manual datatype assignment needed \n')
}
} else if(!is.null(quick_INTS_maxval)) {
if(verbose == TRUE) cat('Selecting compatible datatype for given maximum value \n')
bitDepth = ceiling(log(x = quick_INTS_maxval, base = 2))
}
if(bitDepth > 32 && bitDepth <= 128) {
bitDepth = 32
is_int = FALSE
signed = TRUE
} else if(bitDepth > 128) {
bitDepth = 64
is_int = FALSE
signed = TRUE
}
dataType = NULL
# Determine datatype settings
if(is_int == TRUE) {
if(signed == FALSE) {
if(bitDepth <= 8) {
dataType = 'INT1U'
} else if(bitDepth <= 16) {
dataType = 'INT2U'
} else if(bitDepth <= 32) {
dataType = 'INT4U'
}
} else if(signed == TRUE) {
if(bitDepth <= 16) {
dataType = 'INT2S'
} else if(bitDepth <= 32) {
dataType = 'INT4S'
}
}
} else if(is_int == FALSE) {
if(bitDepth <= 32) {
dataType = 'FLT4S'
} else if(bitDepth == 64) {
dataType = 'FLT8S'
# These are very large numbers. Can't actually tell the difference from FLT4S (less than 2^128) to FLT8S unless you add roughly 10^25 to it.
# This necessary minimum change would be 10^22, but the log used when determining bitDepth further increases the needed difference.
# Manual assignment of dataType could be more reliable than automatic assignment for these very large values.
}
}
return(dataType)
}
#' @title writeGiottoLargeImage
#' @name writeGiottoLargeImage
#' @description Write full resolution image to file. Filetype extension should be
#' included in \code{filename} argument. Be careful if write time and disk space
#' needed if image is very large.
#' @param giottoLargeImage giottoLargeImage object
#' @param gobject giotto object
#' @param largeImage_name name of giottoLargeImage
#' @param filename file name and path to write the image to
#' @param dataType (optional) values for \code{dataType} are "INT1U", "INT2U", "INT2S",
#' "INT4U", "INT4S", "FLT4S", "FLT8S". The first three letters indicate whether
#' the dataType is integer (whole numbers) of a real number (decimal numbers),
#' the fourth character indicates the number of bytes used (allowing for large
#' numbers and/or more precision), and the "S" or "U" indicate whether the values
#' are signed (both negative and positive) or unsigned (positive values only).
#' @param max_intensity (optional) image max intensity value from which \code{dataType}
#' can be automatically determined
#' @param overwrite Overwrite if \code{filename} is already existing
#' @param verbose be verbose
#' @export
writeGiottoLargeImage = function(giottoLargeImage = NULL,
gobject = NULL,
largeImage_name = NULL,
filename = NULL,
dataType = NULL,
max_intensity = NULL,
overwrite = FALSE,
verbose = TRUE) {
# 0. Check params
if(!is.null(giottoLargeImage)) {
if(!inherits(giottoLargeImage, 'giottoLargeImage')) stop('giottoLargeImage argument only accepts giottoLargeImage objects. \n')
}
if(!is.null(max_intensity)) {
if(!is.numeric(max_intensity)) stop('max_intensity must be a numeric \n')
}
if(!is.null(filename)) {
if(!is.character(filename)) stop('filename must be given as character \n')
# check filename
if(file.exists(filename)) {
if(verbose == TRUE) {
if(overwrite == TRUE) cat('File at',filename,'exists.\n (overwrite == TRUE) Image will be overwritten')
if(overwrite == FALSE) cat('File at',filename,'exists.\n (overwrite == FALSE) Image will not be overwritten')
}
}
}
if(is.null(filename)) stop('Please enter a filename to save the image as. \n')
filename = path.expand(filename)
# Match dataType input if given
dataTypeChoices = c('INT1U','INT2U','INT2S','INT4U','INT4S','FLT4S','FLT8S')
if(!is.null(dataType)) dataType = match.arg(dataType, choices = dataTypeChoices)
## 1. get giottoLargeImage if necessary
if(is.null(giottoLargeImage)) {
if(!is.null(gobject) && !is.null(largeImage_name)) {
giottoLargeImage = get_giottoLargeImage(gobject = gobject,
name = largeImage_name)
} else {
stop('either a giottoLargeImage or both the gobject and name of the giottoLargeImage must be given. \n')
}
}
raster_object = giottoLargeImage@raster_object
## 2. Get likely compatible dataType
if(is.null(dataType)) {
dataType = find_terra_writeRaster_dataType(giottoLargeImage = giottoLargeImage,
quick_INTS_maxval = max_intensity)
}
## 3. Write to disk
if(verbose == TRUE) cat(paste0('Writing image to disk as ', dataType))
terra::writeRaster(x = raster_object,
filename = filename,
datatype = dataType,
overwrite = overwrite)
}
#' @title updateGiottoLargeImage
#' @name updateGiottoLargeImage
#' @description Updates the boundaries of a giotto \code{largeImage} object attached to
#' a \code{giotto} object if both \code{gobject} and \code{largeImage_name} params
#' are given. Alternatively can directly accept and return as \code{largeImage}
#' @inheritParams updateGiottoImage
#' @param gobject \code{giotto} object containing giotto \code{largeImage} object
#' @param giottoLargeImage \code{largeImage} object to directly update
#' @param return_gobject return a \code{giotto} object if \code{TRUE}, a giotto
#' \code{largeImage} object if \code{FALSE}
#' @return a \code{giotto} object or an updated giotto \code{largeImage} object if
#' \code{return_gobject = FALSE}
#' @export
updateGiottoLargeImage = function(gobject = NULL,
largeImage_name = NULL,
giottoLargeImage = NULL,
xmax_adj = 0,
xmin_adj = 0,
ymax_adj = 0,
ymin_adj = 0,
x_shift = 0,
y_shift = 0,
scale_factor = NULL,
scale_x = 1,
scale_y = 1,
order = c('first_adj', 'first_scale'), #TODO make this a list of operations to perform, include rotation
xmin_set = NULL,
xmax_set = NULL,
ymin_set = NULL,
ymax_set = NULL,
return_gobject = TRUE,
verbose = TRUE) {
# 0. Check params
# Check input image
if(is.null(gobject)) {
if(is.null(giottoLargeImage)) stop('Image to be updated must be given as gobject AND largeImage_name OR giottoLargeImage argument(s) \n')
if(verbose == TRUE) cat('gobject argument not given \n return_gobject set to FALSE \n')
return_gobject = FALSE
}
if(is.null(giottoLargeImage) && is.null(largeImage_name)) stop('The name of the giottoLargeImage that will be updated needs to be provided \n')
if(!is.null(giottoLargeImage)) {
if(!inherits(giottoLargeImage, 'giottoLargeImage')) stop('giottoLargeImage argument only accpts giottoLargeImage objects \n')
if(verbose == TRUE && !is.null(gobject)) cat('giottoLargeImage argument is given and will take priority \n return_gobject set to FALSE \n')
return_gobject = FALSE
}
# Check scalefactors
if(!is.null(scale_factor)) scale_x = scale_y = scale_factor
# Check spatial anchor values
spatAnchor = c(xmin_set,
xmax_set,
ymin_set,
ymax_set)
if(length(spatAnchor) < 4 && length(spatAnchor) > 0) stop('If set arguments are being used, all four must be given \n')
if(!is.null(spatAnchor)) {
if(xmax_set < xmin_set) stop('xmax_set must be greater than xmin_set \n')
if(ymax_set < ymin_set) stop('ymax_set must be greater than ymin_set \n')
}
# Find order of adjust and scaling
order = match.arg(order, choices = c('first_adj','first_scale'))
# 1. get giottoImage if necessary
if(is.null(giottoLargeImage)) {
if(!is.null(gobject) && !is.null(largeImage_name)) {
g_imageL = get_giottoLargeImage(gobject = gobject,
name = largeImage_name)
} else {
stop('either a giottoLargeImage or both the gobject and name of the giottoLargeImage must be given. \n')
}
}
# 2. Find minmax spatial anchor values if set values not supplied
if(is.null(spatAnchor)) {
spatAnchor = terra::ext(x = g_imageL@raster_object)[1:4] #(xmin, xmax, ymin, ymax)
names(spatAnchor) = NULL
}
# Perform scale if first_scale
if(order == 'first_scale') {
spatAnchor = spatAnchor * c(scale_x, scale_x, scale_y, scale_y)
}
# 3. Prepare adjustment values
# Apply shifts
xmin_adj = xmin_adj - x_shift
xmax_adj = xmax_adj + x_shift
ymin_adj = ymin_adj - y_shift
ymax_adj = ymax_adj + y_shift
# Find final bounds
xmin_final = spatAnchor[1] - xmin_adj
xmax_final = spatAnchor[2] + xmax_adj
ymin_final = spatAnchor[3] - ymin_adj
ymax_final = spatAnchor[4] + ymax_adj
# Perform scale if first_adj
if(order == 'first_adj') {
xmin_final = xmin_final * scale_x
xmax_final = xmax_final * scale_x
ymin_final = ymin_final * scale_y
ymax_final = ymax_final * scale_y
}
# 4. Update the boundaries
if(return_gobject == FALSE) {
if(getNamespaceVersion('terra') >= '1.15-12') g_imageL@raster_object = terra::deepcopy(g_imageL@raster_object)
else {
# g_imageL@raster_object = terra::copy(g_imageL@raster_object)
if(isTRUE(verbose)) warning('\n If largeImage was created from a terra raster
object, manipulations to the giotto image may be
reflected in the raster object as well. Update
terra to >= 1.15-12 to avoid this issue. \n')
}
}
terra::ext(g_imageL@raster_object) = c(xmin_final,
xmax_final,
ymin_final,
ymax_final)
# Update the extent tracking slot
g_imageL@extent = as.vector(terra::ext(g_imageL@raster_object))
#5. Update the scalefactors for x and y
g_imageL@resolution = terra::res(g_imageL@raster_object) #(x,y)
names(g_imageL@resolution) = c('x','y')
g_imageL@scale_factor = (1/g_imageL@resolution)
if(return_gobject == TRUE) {
gobject@largeImages[[largeImage_name]] = g_imageL
return(gobject)
} else {
return(g_imageL)
}
}
#' @title addGiottoLargeImage
#' @name addGiottoLargeImage
#' @description Adds giotto image objects to your giotto object
#' @param gobject giotto object
#' @param largeImages list of giottoLargeImage objects
#' @param spat_loc_name provide spatial location slot in Giotto to align images. (optional)
#' @param scale_factor provide scale of image pixel dimensions relative to spatial coordinates.
#' @param negative_y map image to negative y spatial values if TRUE during automatic alignment. Meaning that origin is in upper left instead of lower left.
#' @param verbose be verbose
#' @return an updated Giotto object with access to the list of images
#' @export
addGiottoLargeImage = function(gobject = NULL,
largeImages = NULL,
spat_loc_name = NULL,
scale_factor = NULL,
negative_y = TRUE,
verbose = TRUE) {
# 0. check params
if(is.null(gobject)) stop('The giotto object that will be updated needs to be provided')
if(is.null(largeImages)) stop('The giotto large image(s) that will be added needs to be provided')
ext_scale_factor = FALSE
if(!is.null(scale_factor)) {
if(!is.numeric(scale_factor)) stop ('Given scale_factor(s) must be numeric')
if((length(scale_factor) == length(largeImages)) || length(scale_factor) == 1) {
cat('scale_factor(s) external to giottoImage have been given and will be used')
ext_scale_factor = TRUE
} else {
stop('if scale_factor is given, it must be a numeric with either a single value or as many values as there are largeImages are provided')
}
}
# 1. expand scale_factors
if(ext_scale_factor == TRUE) {
if(length(scale_factor == 1)) {
scale_factor = rep(scale_factor, length(largeImages))
}
}
# 2. Add image with for loop
for(image_i in 1:length(largeImages)) {
im = largeImages[[image_i]]
if(inherits(im, 'giottoLargeImage')) {
im_name = im@name
all_im_names = names(gobject@largeImages)
if(im_name %in% all_im_names) {
cat('\n ', im_name, ' has already been used, will be overwritten \n')
}
# Deep copy the raster_object
if(getNamespaceVersion('terra') >= '1.15-12') im@raster_object = terra::deepcopy(im@raster_object)
else {
# im@raster_object = terra::copy(im@raster_object)
if(isTRUE(verbose)) warning('\n If largeImage was created from a terra raster
object, manipulations to the giotto image may be
reflected in the raster object as well. Update
terra to >= 1.15-12 to avoid this issue. \n')
}
# # 3. Update boundaries if not already done during createGiottoImage() due to lack of spatlocs and gobject
# if(sum(im@boundaries == c(0,0,0,0)) == 4 && sum(im@minmax == c(10,0,10,0)) == 4) {
# if(!is.null(spat_loc_name)) { # A check for the first available spatloc was already done
# spatlocs = get_spatial_locations(gobject = gobject,
# spat_loc_name = spat_loc_name)
#
# #Find spatial minmax values
# xmin_sloc = min(spatlocs$sdimx)
# xmax_sloc = max(spatlocs$sdimx)
# ymin_sloc = min(spatlocs$sdimy)
# ymax_sloc = max(spatlocs$sdimy)
#
# #Find adjustment values
# img_minmax = get_img_minmax(mg_img = im@mg_object,
# negative_y = negative_y)
# if(ext_scale_factor == TRUE) {
# adj_values = get_adj_rescale_img(img_minmax = img_minmax,
# spatial_locs = spatlocs,
# scale_factor = scale_factor[[image_i]])
# } else if (ext_scale_factor == FALSE) {
# adj_values = get_adj_rescale_img(img_minmax = img_minmax,
# spatial_locs = spatlocs,
# scale_factor = im@scale_factor[[spat_loc_name]])
# }
#
# #Add minmax values to giottoImage@minmax
# im@minmax = c('xmax_sloc' = xmax_sloc,
# 'xmin_sloc' = xmin_sloc,
# 'ymax_sloc' = ymax_sloc,
# 'ymin_sloc' = ymin_sloc)
#
# #Add adjustment values to giottoImage@boundaries
# im@boundaries = c('xmax_adj' = as.numeric(adj_values[['xmax_adj_orig']]),
# 'xmin_adj' = as.numeric(adj_values[['xmin_adj_orig']]),
# 'ymax_adj' = as.numeric(adj_values[['ymax_adj_orig']]),
# 'ymin_adj' = as.numeric(adj_values[['ymin_adj_orig']]))
#
# # Inherit external scaling factors if given
# if(ext_scale_factor == TRUE) {
# im@scale_factor[[spat_loc_name]] = scale_factor[[image_i]]
# im@resolution[[spat_loc_name]] = 1/(scale_factor[[image_i]])
# }
# ## Externally given scale_factors will only be written in/used if boundary adj values are not pre-existing
# }
#
# }
# 4. Add giottoImage to gobject
gobject@largeImages[[im_name]] = im
} else {
warning('image [',image_i,'] is not a giotto image object')
}
}
return(gobject)
}
#' @title reconnect_giottoLargeImage
#' @name reconnect_giottoLargeImage
#' @description reconnect giottoLargeImage if image pointer is broken
#' @param giottoLargeImage giottoLargeImage to reconnect
#' @param image_path path to source file of giottoLargeImage
#' @return reconnected giottoLargeImage
#' @keywords internal
reconnect_giottoLargeImage = function(giottoLargeImage,
image_path) {
# load in new terra raster objects
raster_object = create_terra_spatRaster(image_path = image_path)
# replace old raster objects and inherit tracked extents
giottoLargeImage@raster_object = raster_object
terra::ext(giottoLargeImage@raster_object) = giottoLargeImage@extent
# return reconnected giottoLargeImage
return(giottoLargeImage)
}
# giottoMultiIntensity tools ####
# rvision and ROpenCVLite tools ####
# Generalized Image Tools ####
#' @title Plot a giotto image object
#' @name plotGiottoImage
#' @description Display a giotto image in the viewer panel. Image object to plot
#' can be specified by providing the giotto object containing the image (\code{gobject}),
#' the image object name (\code{image_name}), and the image object type (\code{image_type}).
#' Alternatively, image objects can be directly plotted through their respective
#' associated params.
#' @param gobject gobject containing giotto image object
#' @param image_name name of giotto image object
#' @param image_type type of giotto image object to plot
#' @param giottoImage giottoImage object to plot directly
#' @param giottoLargeImage giottoLargeImage object to plot directly
#' @param largeImage_crop_params_list (optional) named list of params for focusing
#' on a specified region of a giottoLargeImage.
#' @param largeImage_max_intensity (optional) assign override value to treat as
#' maximum intensity in color scale when plotting giottoLargeImage
#' @param ... additional params to pass to image object specific plotting functions
#' @section largeImage-specific additional params:
#' \code{largeImage_crop_params_list} accepts a named list of the following
#' possible params to define a region of interest (ROI) to plot through either
#' a terra extent object OR x and y min and max bounds given as numerics:
#' \itemize{
#' \item{\code{crop_extent} -- terra extent object to define crop ROI}
#' \item{\code{xmax_crop} -- x max of ROI}
#' \item{\code{xmin_crop} -- x min of ROI}
#' \item{\code{ymax_crop} -- y max of ROI}
#' \item{\code{ymin_crop} -- y min of ROI}
#' }
#' \code{largeImage_max_intensity} accepts a numeric value to set the max
#' value in the plotting color scale. Can be used in case there are high
#' outlier intensity values in the image and a preview with alternative
#' color scaling is desired.
#' @family basic image functions
#' @export
plotGiottoImage = function(gobject = NULL,
image_name = NULL,
image_type = NULL,
giottoImage = NULL,
giottoLargeImage = NULL,
largeImage_crop_params_list = NULL,
largeImage_max_intensity = NULL,
...) {
# Check params
if(!is.null(giottoImage) && !is.null(giottoLargeImage)) stop('Only one of a giottoImage or a giottoLargeImage can be plotted at the same time. \n')
# Get image object
if(!is.null(gobject)) {
img_obj = get_giottoImage(gobject = gobject,
image_type = image_type,
name = image_name)
}
if(!is.null(giottoImage)) {
img_obj = giottoImage
image_type = 'image'
image_name = img_obj@name
}
if(!is.null(giottoLargeImage)) {
img_obj = giottoLargeImage
image_type = 'largeImage'
image_name = img_obj@name
}
# Select plotting function
cat('Plotting ', image_type, ': "', image_name, '" ... \n', sep = '')
if(image_type == 'image') {
plot_giottoImage_MG(giottoImage = img_obj)
}
if(image_type == 'largeImage') {
plot_giottoLargeImage(giottoLargeImage = img_obj,
crop_extent = largeImage_crop_params_list$crop_extent,
xmax_crop = largeImage_crop_params_list$xmax_crop,
xmin_crop = largeImage_crop_params_list$xmin_crop,
ymax_crop = largeImage_crop_params_list$ymax_crop,
ymin_crop = largeImage_crop_params_list$ymin_crop,
max_intensity = largeImage_max_intensity,
...)
}
}
#' @title addGiottoImage
#' @name addGiottoImage
#' @description Adds lists of giottoImages and giottoLargeImages to gobjects
#' @param gobject gobject to add images objects to
#' @param images list of giottoImages to add
#' @param largeImages list of giottoLargeImages to add
#' @param spat_loc_name provide spatial location slot in Giotto to align giottoImages. Defaults to first one
#' @param scale_factor provide scale of image pixel dimensions relative to spatial coordinates.
#' @param negative_y Map image to negative y spatial values if TRUE during automatic alignment. Meaning that origin is in upper left instead of lower left.
#' @return an updated Giotto object with access to the list of images
#' @family basic image functions
#' @export
addGiottoImage = function(gobject = NULL,
images = NULL,
largeImages = NULL,
spat_loc_name = NULL,
scale_factor = NULL,
negative_y = TRUE) {
if(!is.null(images) && !is.null(largeImages)) stop('Can only add one type of image to giotto object at a time')
if(!is.null(images)) {
addGiottoImageMG(gobject = gobject,
images = images,
spat_loc_name = spat_loc_name,
scale_factor = scale_factor,
negative_y = negative_y)
} else if(!is.null(largeImages)) {
addGiottoLargeImage(gobject = gobject,
largeImages = largeImages,
spat_loc_name = spat_loc_name,
scale_factor = scale_factor,
negative_y = negative_y)
}
}
#' @title updateGiottoImage
#' @name updateGiottoImage
#' @description Updates the spatial positioning and sizing of a giotto \code{image} or
#' \code{largeImage} attached to a giotto object.
#' @details This function works for all image objects associated with Giotto.
#' @param gobject gobject containing desired image object
#' @param image_name name of giotto \code{image} object
#' @param largeImage_name name of giotto \code{largeImage} object
#' @param xmax_adj,xmin_adj,ymax_adj,ymin_adj adjust image boundaries by increasing
#' maximum and decreasing minimum bounds respectively of xy bounds
#' @param x_shift,y_shift shift entire image along xy axes
#' @param scale_factor set \code{scale_x} and \code{scale_y} params at the same time
#' @param scale_x,scale_y independently scale x or y axis image mapping from coordinate origin
#' @param order order of operations between fine adjustments (adjustment and shift
#' parameters) and scaling
#' @param xmin_set,xmax_set,ymin_set,ymax_set directly set xy image boundaries.
#' Overrides minmax values as spatial anchor.
#' @param return_gobject return a giotto object if \code{TRUE}, a giotto image object
#' if \code{FALSE}
#' @param verbose be verbose
#' @return a giotto object or an updated giotto image object if return_gobject = F
#' @family basic image functions
#' @export
updateGiottoImage = function(gobject = NULL,
image_name = NULL,
largeImage_name = NULL,
xmax_adj = 0,
xmin_adj = 0,
ymax_adj = 0,
ymin_adj = 0,
x_shift = 0,
y_shift = 0,
scale_factor = NULL,
scale_x = 1,
scale_y = 1,
order = c('first_adj', 'first_scale'),
xmax_set = NULL,
xmin_set = NULL,
ymax_set = NULL,
ymin_set = NULL,
return_gobject = TRUE,
verbose = TRUE) {
# 0. Check params
if(is.null(gobject)) stop('The giotto object that will be updated needs to be provided \n')
if(is.null(image_name) && is.null(largeImage_name)) stop('The name of the giotto image that will be updated needs to be provided \n')
order = match.arg(order, choices = c('first_adj','first_scale'))
if(!is.null(image_name) && !is.null(largeImage_name)) stop('Adjust only giottoImage OR giottoLargeImage at any one time. \n')
# 2. Select adjustment function
if(!is.null(image_name)) {
out = updateGiottoImageMG(gobject = gobject,
image_name = image_name,
xmax_adj = xmax_adj,
xmin_adj = xmin_adj,
ymax_adj = ymax_adj,
ymin_adj = ymin_adj,
x_shift = x_shift,
y_shift = y_shift,
scale_factor = scale_factor,
scale_x = scale_x,
scale_y = scale_y,
order = order,
xmax_set = xmax_set,
xmin_set = xmin_set,
ymax_set = ymax_set,
ymin_set = ymin_set,
return_gobject = return_gobject,
verbose = verbose)
} else if(!is.null(largeImage_name)) {
out = updateGiottoLargeImage(gobject = gobject,
largeImage_name = largeImage_name,
xmax_adj = xmax_adj,
xmin_adj = xmin_adj,
ymax_adj = ymax_adj,
ymin_adj = ymin_adj,
x_shift = x_shift,
y_shift = y_shift,
scale_factor = scale_factor,
scale_x = scale_x,
scale_y = scale_y,
order = order,
xmax_set = xmax_set,
xmin_set = xmin_set,
ymax_set = ymax_set,
ymin_set = ymin_set,
return_gobject = return_gobject,
verbose = verbose)
}
return(out)
}
#' @title reconnect_image_object
#' @name reconnect_image_object
#' @description Reconnect giotto image object with dead image pointer using a filepath
#' to the original image source
#' @details This is a simple wrapper function for image object-specific reconnection
#' functions and does not include other functionality to find the specific image
#' objects in the giotto object.
#' @param image_object giotto image object
#' @param image_type type of giotto image object
#' @param image_path path to image source to reconnect image object with
#' @return reconnected image_object
#' @keywords internal
reconnect_image_object = function(image_object,
image_type,
image_path) {
if(image_type == 'image') image_object = reconnect_giottoImage_MG(giottoImage = image_object,
image_path = image_path)
if(image_type == 'largeImage') image_object = reconnect_giottoLargeImage(giottoLargeImage = image_object,
image_path = image_path)
return(image_object)
}
#' @title Reconnect images with dead pointers
#' @name reconnectGiottoImage
#' @description reconnect a gobject's dead image pointers using filepaths to
#' the original source image files
#' @details Inputs can either be given as both image name (\code{image_name}/\code{largeImage_name})
#' and filepath (\code{image_path}/\code{largeImage_path}) args or as only a
#' a named list through a filepath argument alone.
#' If \code{auto_reconnect = TRUE} then no additional params need to be supplied.
#' As long as giotto image objects were directly created using filepaths, those
#' filepaths are stored within the image objects and will be referenced during
#' reconnection. Issues will only arise if giotto image objects were created directly
#' from the underlying image handling package objects (\emph{magick} or \emph{raster objects}) or
#' if image files have been moved since the the giotto image object was generated.
#' In such cases, use manual reconnection by setting \code{auto_reconnect = FALSE}.
#' @param gobject giotto object
#' @param auto_reconnect automatically reconnect images if TRUE. manual if FALSE
#' @param reconnect_type type of image to reconnect when auto_reconnect = TRUE
#' @param image_name names of images to reconnect
#' @param largeImage_name name of large images to reconnect
#' @param image_path named list of paths to images to reconnect to giottoImages
#' @param largeImage_path named list of paths to images to reconnect to giottoLargeImages
#' @param verbose be verbose
#' @return a giotto object with updated image pointer
#' @family basic image functions
#' @export
reconnectGiottoImage = function(gobject,
auto_reconnect = TRUE,
reconnect_type = c('all', 'image', 'largeImage'),
image_name = NULL,
largeImage_name = NULL,
image_path = NULL,
largeImage_path = NULL,
verbose = TRUE) {
# Adding image_types:
# Manual workflow needs to be updated when adding more image types
if(is.null(gobject)) stop('Giotto object containing the giottoImages or giottoLargeImages to reconnect must be given \n')
reconnect_type = match.arg(reconnect_type, choices = c('all', 'image', 'largeImage'))
# 1. Find names and locations of image objects in gobject: ----------------------------#
if(reconnect_type %in% c('image', 'largeImage')) {
availableImgs = list_images(gobject = gobject, img_type = reconnect_type)
} else if(reconnect_type == 'all') {
availableImgs = list_images(gobject = gobject)
} else {
default = stop('reconnect_type input unrecognized')
}
# 2. Get reconnection info: -----------------------------------------------------------#
# Initialize lists
name_list = list()
img_list = list()
img_path = list()
## image_type_list | vector of image types present within images to be reconnected (vector)
## name_list | determines which available images to reconnect, categorized by image_type | (list)
## img_list | list of image objects, categorized by image_type | (list)
## img_path | filepaths to reconnect images with, categorized by image_type | (list)
#### Auto Workflow
if(auto_reconnect == TRUE) {
if(verbose == TRUE) cat('Attempting automatic reconnection...\n\n')
# Find image_types to reconnect
image_type_list = unique(availableImgs$img_type)
# Run for each image_type given...
for(image_type in image_type_list) {
# Images to update will be ANY available images
# Get list of image object names
name_list[[image_type]] = list_images_names(gobject = gobject,
img_type = image_type)
# get image objects
img_list[[image_type]] = lapply(X = name_list[[image_type]],
FUN = get_giottoImage,
gobject = gobject,
image_type = image_type)
# get file paths from image objects
img_path[[image_type]] = lapply(X = 1:length(img_list[[image_type]]),
function(x) {
img_list[[image_type]][[x]]@file_path
})
# print discovered images and paths
# additionally, set path to NULL if file.exists() == FALSE
if(verbose == TRUE) cat(image_type, '(s) discovered...\n', sep = '')
for(image_i in 1:length(img_path[[image_type]])) {
if(!is.null(img_path[[image_type]][[image_i]])) {
if(verbose == TRUE) cat('-->', name_list[[image_type]][[image_i]], ': filepath found')
if(!file.exists(img_path[[image_type]][[image_i]])) {
if(verbose == TRUE) cat('but file is missing\n')
img_path[[image_type]][[image_i]] = NULL
} else if(verbose == TRUE) cat('\n')
} else if(verbose == TRUE) cat('-->', name_list[[image_type]][[image_i]], ': filepath NOT found\n')
}
if(verbose == TRUE) cat('\n')
} # image_type end loop
#### Manual Workflow
} else {
if(verbose == TRUE) cat('Reconnecting with manual input...\n\n')
# Check params
# filepath list(s) must be given as input
if(is.null(image_path) & is.null(largeImage_path)) stop('Image filepaths must be given for manual reconnection \n')
# Assemble reconnection info lists - UPDATE THIS WHEN ADDING IMAGE TYPES
image_type_list = c()
if(!is.null(image_path)) {
image_type_list = c(image_type_list, 'image')
name_list[['image']] = image_name
img_path[['image']] = image_path
}
if(!is.null(largeImage_path)) {
image_type_list = c(image_type_list, 'largeImage')
name_list[['largeImage']] = largeImage_name
img_path[['largeImage']] = largeImage_path
}
# Run for each image_type given...
for(image_type in image_type_list) {
# Check params
if(is.null(name_list[[image_type]])) name_list[[image_type]] = names(img_path[[image_type]])
if(is.null(name_list[[image_type]])) stop('Names of ',image_type,'s to be reconnected must be given as named list in ',image_type,'_path arg or together with ',image_type,'_path as a separate vector in ',image_type,'_name arg. \n')
if(length(unique(name_list[[image_type]])) != length(img_path[[image_type]])) stop('If ',image_type,'s to be reconnected are selected through ',image_type,'_name arg then names must be unique and length and order must be the same as in ',image_type,'_path arg. \n')
if(!all(name_list[[image_type]] %in% list_images_names(gobject = gobject, img_type = image_type))) stop('Names given to ',image_type,'_name argument must match those in the gobject \n')
# get image objects
img_list[[image_type]] = lapply(X = name_list[[image_type]],
FUN = get_giottoImage,
gobject = gobject,
image_type = image_type)
# update file_path
img_list[[image_type]] = lapply(X = 1:length(img_list[[image_type]]),
function(x) {
img_list[[image_type]][[x]]@file_path = img_path[[image_type]][[x]]
})
}
} # Manual workflow-specific end
# 3. Load new image pointer: -----------------------------------------------------------#
image_path_NULL = list()
for(image_type in image_type_list) {
# check for NULLs in image paths
image_path_NULL[[image_type]] = unlist(lapply(X = img_path[[image_type]], is.null))
# Remove NULL path entries from name, path, and image object lists
if(any(image_path_NULL[[image_type]])) {
# End loop early if there are no imagepaths
if(sum(image_path_NULL[[image_type]]) == length(img_path[[image_type]])) {
if(verbose == TRUE) cat(image_type, ': no filepaths found. Skipping. \n')
next
}
if(verbose == TRUE) {
cat('\n Skipping ',image_type,'s with missing filepaths: \n', sep = '')
for(image_NULL_i in 1:sum(image_path_NULL[[image_type]])) {
cat(name_list[[image_type]][[which(image_path_NULL[[image_type]])[[image_NULL_i]]]], '\n')
}
}
img_path[[image_type]] = img_path[[image_type]][!(image_path_NULL[[image_type]])]
name_list[[image_type]] = name_list[[image_type]][!(image_path_NULL[[image_type]])]
img_list[[image_type]] = img_list[[image_type]][!(image_path_NULL[[image_type]])]
}
# Load pointers
img_list[[image_type]] = lapply(X = 1:length(img_list[[image_type]]),
function(x) {
reconnect_image_object(image_object = img_list[[image_type]][[x]],
image_type = image_type,
image_path = img_path[[image_type]][[x]])
})
# 4. Update gobject:--------------------------------------------------------------------#
# Set the image objects into the gobject
for(image_ii in 1:length(img_list[[image_type]])) {
gobject = set_giottoImage(gobject = gobject,
image = img_list[[image_type]][[image_ii]],
image_type = image_type,
name = name_list[[image_type]][[image_ii]],
verbose = FALSE)
}
if(verbose == TRUE) cat('done \n')
} # image_type end loop
return(gobject)
}
#' @title Plot distribution of image intensity values
#' @name distGiottoImage
#' @description Plot distribution of intensity values using either a density plot
#' or a histogram. Useful for finding image artefact outliers and determining
#' reasonable scaling cutoffs.
#' @section Plotting method \code{'dens'}:
#' Density plot of intensity values for image objects. \strong{N} total values
#' examined. \strong{Bandwidth} refers to the curve smoothing value applied.
#' @section Plotting method \code{'hist'}:
#' Histogram of intensity values for image objects.
#' @details Plot is generated from a downsampling of the original image
#' @param gobject giotto object
#' @param image_type image object type (only supports largeImage and is set as
#' default)
#' @param image_name name of image object to use
#' @param giottoLargeImage giotto large image object
#' @param method plot type to show image intensity distribution
#' @export
distGiottoImage = function(gobject = NULL,
image_type = 'largeImage',
image_name = NULL,
giottoLargeImage = NULL,
method = c('dens', 'hist')) {
# check params
if(image_type != 'largeImage') stop('Only largeImage objects currently supported \n')
if((is.null(image_type) | is.null(image_name) | is.null(gobject)) & (is.null(giottoLargeImage))) {
stop('Image must be given through gobject, image_type, and image_name params or as the image object itself \n')
}
method = match.arg(arg = method, choices = c('dens','hist'))
# run specific function
if(image_type == 'largeImage') {
dist_giottoLargeImage(gobject = gobject,
image_name = image_name,
giottoLargeImage = giottoLargeImage,
method = method)
}
}
#' @title Add alpha channel to image array
#' @name add_img_array_alpha
#' @details Add 4th alpha channel to 3 channel RGB image arrays
#' @param x image array to use
#' @param alpha global alpha value to use. Numeric. Scales from 0 to 1, with 0
#' being fully transparent and 1 being fully visible
#' @return image array with 4th channel for transparency
#' @keywords internal
add_img_array_alpha = function(x,
alpha) {
img_dims = dim(x)
x_alpha = array(data = alpha, dim = c(img_dims[1], img_dims[2], 4))
x_alpha[,,1:3] = x
return(x_alpha)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.