Create multi-panel figure using PNG/JPEG images - r

Problem:
I want to make a multi-panel figure using PNG or JPEG images. The images were not created in R but I want to patch them together in R to form one figure. All the images are the same size/dimensions.
What I've tried:
library(png)
img1 <- readPNG("filepath/img1.png")
img2 <- readPNG("filepath/img2.png")
library(patchwork)
patch <- img1 + img2
patch
When I run this, I get the following error:
[ reached getOption("max.print") -- omitted 3 matrix slice(s) ]
I increased the max print multiple times (to ridiculously high numbers):
options(maxprint = 1000000000000)
But still get the same error.
I then tried making each image into a ggplot (without the points) using:
library(ggplot2)
img1plot <- ggplot() +
background_image(img1) +
theme(plot.margin = margin(t=1, l=1, r=1, b=1, unit = "cm"))
Which returns the following error:
Error in background_image(d311) :
could not find function "background_image"
Is there another way to patch images together in R to make a figure?
Edit:
Based on the comment from #davidnortes, I tried the following:
p1 <- ggplot2::annotation_custom(grid::rasterGrob(img1,
width=ggplot2::unit(1,"npc"),
height=ggplot2::unit(1,"npc")),
-Inf, Inf, -Inf, Inf)
p2 <- ggplot2::annotation_custom(grid::rasterGrob(img2,
width=ggplot2::unit(1,"npc"),
height=ggplot2::unit(1,"npc")),
-Inf, Inf, -Inf, Inf)
library(cowplot)
plots <- plot_grid(
p1, p2,
labels = c('A', 'B'),
align="hv"
)
plots
I get the following warning messages and the plot doesn't form:
Warning messages:
1: In as_grob.default(plot) :
Cannot convert object of class LayerInstanceLayerggprotogg into a grob.
2: In as_grob.default(plot) :
Cannot convert object of class LayerInstanceLayerggprotogg into a grob.

I'm giving you the couple of alternatives that I Use the most:
Alternative 1: combination of ggplot2, grid and cowplot.
Each of your PNG image can be embedded in a ggplot object using:
ggplot2::ggplot() + ggplot2::annotation_custom(grid::rasterGrob(YourPNGimage,
width=ggplot2::unit(1,"npc"),
height=ggplot2::unit(1,"npc")),
-Inf, Inf, -Inf, Inf)
Then you can use cowplot::plot_grid() to arrange your plots.
Alternative 2: using magick package.
The package counts on its own functions to read images, thus we need to tweak your code a bit:
img1 <- magick::image_read("filepath/img1.png")
img2 <- magick::image_read("filepath/img2.png")
Then using functions like magick::image_append(c(img1, img2)) you can combine them. See the magick package documentation for more info.

You can also rbind image arrays. But as they are 3D (x,y,RGB) you must use abind function from abind package. along=1 to bind vertically, 2 horizontally.
Works because image have same size.
img1 <- readPNG("filepath/img1.png")
img2 <- readPNG("filepath/img2.png")
img12 <- abind::abind(img1,img2,along=1)
png::writePNG(img12,"filepath/img12.png")

You can use magick package in R to do a collage.
# read the the png files into a list
pngfiles <-
list.files(
path = here::here("png_ouput_folder"),
recursive = TRUE,
pattern = "\\.png$",
full.names = T
)
# read images and then create a montage
# tile =2 , means arrange the images in 2 columns
# geometry controls the pixel sixe, spacing between each image in the collage output.
magick::image_read(pngfiles) %>%
magick::image_montage(tile = "2", geometry = "x500+10+5") %>%
magick::image_convert("jpg") %>%
magick::image_write(
format = ".jpg", path = here::here(paste(name,"_collage.jpg",sep="")),
quality = 100
)

As others have suggested the magick package is much simpler than a low-level solutions using grobs and related. magick is powerful but IMHO the documentation is poor and very circular.
However, there is a simple solution to your question in the page for image_montage(). The most important argument is the geometry specification, which governs the spacing between the "tiles."
library(magick)
input <- rep(logo, 12)
image_montage(input, geometry = 'x100+10+10', tile = '4x3', bg = 'pink', shadow = TRUE)
To get no spacing at all, use geometry = '0x100+0+0', shadow = FALSE".

Related

change resolution of an object formal class RasterLayer sourced from a .tif

Hej all,
I am trying to create a rasterLayer object in R from a .tif file.
I am using the raster() function from the raster package. I tried all commands within that function (specifing ncol, nrow, res, ...) but the image I get is always highly pixelated (while the original .tif is very sharp).
Can anyone help me find a way to load the file so that the restult is sharp as well?
THanks for all help!!
[How it should look like (apart from the colours)][1]
[How it looks like][2]
The file can be found here: https://www.dropbox.com/s/2x58sbjgamkh15f/mammal_richness.tif?dl=0
#load data
x <- "./mammal_richness.tif"
RasterData <- raster(x)
#plot data
worldPlot <- ggplot() +
geom_raster(data= RasterData, aes(x = long, y = lat, fill = mammal_richness))+
scale_fill_gradient("Mammal richness",
low = "grey96", high = "black", na.value = "grey96",
guide = guide_colourbar(direction = "horizontal", barwidth = 10,
title.position = "top"))```
[1]: https://i.stack.imgur.com/vTG88.png
[2]: https://i.stack.imgur.com/AwtZ7.png
EDIT - solution
It turns out, the RasterData object is fine ( plot(RasterData) results in a sharp image). However, to force ggplot to use the full data available, it is necessary to change the maxpixel argument in the fortify function:
RasterData_big <- fortify(RasterData, maxpixels = 1e6)
This will take a while, but afterwards the image is nice and sharp.

From rastermap package to ggplot2

My problem: I want to draw a map obtained via rastermap package with ggplot2.
Searching for alternatives to ggmap package I found the rastermap package which provides an easy way to obtain maps from external sources. The readme provides a very simple example:
# install.packages("devtools")
devtools::install_github("hadley/rastermap")
houston <- fetch_region(c(-95.80204, -94.92313), c(29.38048, 30.14344),
stamen("terrain"))
houston
plot(houston)
The problem comes whether I try to plot using ggplot. So far I've tried several ways but none of them seems to work. Is it possible? Any idea?
rastermap generates a matrix of colours in hexadecimal strings (#RRGGBB format). It may be simplest to convert this to a more common form for spatial data, a multiband raster brick, with separate layers for the red, green and blue.
We can write a short helper function to convert hexadecimal strings into the separate integer values (i.e. this is the reverse of the rgb() function):
un_rgb = function (x) {
x = unlist(str_split(x, ''))
r = paste0(x[2], x[3])
g = paste0(x[4], x[5])
b = paste0(x[6], x[7])
strtoi(c(r,g,b), 16)
}
Using this function we convert the rastermap matrix into a three band raster brick:
library(raster)
m = as.matrix(houston)
l=lapply(m[], un_rgb)
r=g=b=matrix(, dim(m)[1], dim(m)[2])
r[] = sapply(l, function(i) i[1])
g[] = sapply(l, function(i) i[2])
b[] = sapply(l, function(i) i[3])
rgb.brick = brick(raster(r), raster(g), raster(b))
And set the extent of the new raster to that of the original rastermap
extent(rgb.brick) = extent(matrix(unlist(attributes(houston)$bb), nrow=2))
Now that we have a more usual form of raster object, we can do various things with it. For example, we can plot it in ggplot using library(RStoolbox):
ggRGB(rgb.brick, r=1, g=2, b=3)
Or we can save it as an image to use as an annotation background in ggplot:
png('test.png', dim(rgb.brick)[2], dim(rgb.brick)[1])
plotRGB(rgb.brick, 1, 2, 3)
dev.off()
img <- png::readPNG("test.png")
gr <- grid::rasterGrob(img, interpolate=TRUE)
ggplot() + annotation_custom(gr, -Inf, Inf, -Inf, Inf)
Why would you want an alternative? You can get a stamen map from ggmap:
library(ggmap)
ggmap(get_stamenmap(c(-95.80204, 29.38048, -94.92313, 30.14344))) +
# some points to plot
geom_point(aes(x = seq(-95.5, -95.2, 0.1), y = seq(29.7, 30, 0.1)), color = "red")

combine multiple plots to a gif

Im trying to use the caTools package to combine multiple plots into a gif.
My basic code looks like :
for( i in 1:100){
plot(....) // plots few points and lines, changes slightly with each i
}
I would like to combine these to a gif to see the "evolution" of the plot.
However for write.gif() from caTools, I need to give an image as an input.
For each i, how do I convert the plot into an image without
saving to disc as an intermediate step
depending on ImageMagick or similar external dependencies.
Please feel free to point out if this is a duplicate. [ Creating a Movie from a Series of Plots in R doesnt seem to answer this ]
EDIT: Basically this requires us to convert a plot to a matrix. Since this very likely happens every time someone saves a plot, it should not be very difficult. However Im not able to get hold of how to exactly do this.
I suggest using the animation package and ImageMagick instead:
library(animation)
## make sure ImageMagick has been installed in your system
saveGIF({
for (i in 1:10) plot(runif(10), ylim = 0:1)
})
Otherwise you could try it in the veins of this (plenty of room for optimization):
library(png)
library(caTools)
library(abind)
# create gif frames and write them to pngs in a temp dir
dir.create(dir <- tempfile(""))
for (i in 1:8) {
png(file.path(dir, paste0(sprintf("%04d", i), ".png")))
plot(runif(10), ylim = 0:1, col = i)
dev.off()
}
# read pngs, create global palette, convert rasters to integer arrays and write animated gif
imgs <- lapply(list.files(dir, full.names = T), function(fn) as.raster(readPNG(fn)))
frames <- abind(imgs, along = 3) # combine raster pngs in list to an array
cols <- unique(as.vector(frames)) # determine unique colors, should be less then 257
frames <- aperm(array(match(frames, cols) - 1, dim = dim(frames)), c(2,1,3)) # replace rgb color codes (#ffffff) by integer indices in cols, beginning with 0 (note: array has to be transposed again, otherwise images are flipped)
write.gif(
image = frames, # array of integers
filename = tf <- tempfile(fileext = ".gif"), # create temporary filename
delay = 100, # 100/100=1 second delay between frames
col = c(cols, rep("#FFFFFF", 256-length(cols))) # color palette with 256 colors (fill unused color indices with white)
)
# open gif (windows)
shell.exec(tf)
Is that what you are looking for?
library(ggplot2)
a <- 0:10
df <- data.frame(a=a,b=a)
steps <-function(end){
a <- ggplot(df[1:end,], aes(a,b)) +
geom_point() +
scale_x_continuous(limits=c(0,10)) +
scale_y_continuous(limits=c(0,10))
print(a)
}
gif <- function() {
lapply(seq(1,11,1), function(i) {
steps(i)
})
}
library(animation)
saveGIF(gif(), interval = .2, movie.name="test.gif")
I liked #ttlngr's answer but I wanted something a bit simpler (it still uses ImageMagick).
saveGIF({
for (i in 1:10){
a <- ggplot(df[1:i,], aes(a,b)) +
geom_point() +
scale_x_continuous(limits=c(0,10)) +
scale_y_continuous(limits=c(0,10))
print(a)}
}, interval = .2, movie.name="test.gif")

R ggplot: geom_tile lines in pdf output

I'm constructing a plot that uses geom_tile and then outputting it to .pdf (using pdf("filename",...)). However, when I do, the .pdf result has tiny lines (striations, as one person put it) running through it. I've attached an image showing the problem.
Googling let to this thread, but the only real advice in there was to try passing size=0 to geom_tile, which I did with no effect. Any suggestions on how I can fix these? I'd like to use this as a figure in a paper, but it's not going to work like this.
Minimal code:
require(ggplot2)
require(scales)
require(reshape)
volcano3d <- melt(volcano)
names(volcano3d) <- c("x", "y", "z")
v <- ggplot(volcano3d, aes(x, y, z = z))
pdf("mew.pdf")
print(v + geom_tile(aes(fill=z)) + stat_contour(size=2) + scale_fill_gradient("z"))
This happens because the default colour of the tiles in geom_tile seems to be white.
To fix this, you need to map the colour to z in the same way as fill.
print(v +
geom_tile(aes(fill=z, colour=z), size=1) +
stat_contour(size=2) +
scale_fill_gradient("z")
)
Try to use geom_raster:
pdf("mew.pdf")
print(v + geom_raster(aes(fill=z)) + stat_contour(size=2) + scale_fill_gradient("z"))
dev.off()
good quality in my environment.
I cannot reproduce the problem on my computer (Windows 7), but I remember it was a problem discussed on the list for certain configurations. Brian Ripley (if I remember) recommended
CairoPDF("mew.pdf") # Package Cairo
to get around this
In the interests of skinning this cat, and going into waaay too much detail, this code decomposes the R image into a mesh of quads (as used by rgl), and then shows the difference between a raster plot and a "tile" or "rect" plot.
library(raster)
im <- raster::raster(volcano)
## this is the image in rgl corner-vertex form
msh <- quadmesh::quadmesh(im)
## manual labour for colour scaling
dif <- diff(range(values(im)))
mn <- min(values(im))
scl <- function(x) (x - mn)/dif
This the the traditional R 'image', which draws a little tile or 'rect()' for every pixel.
list_image <- list(x = xFromCol(im), y = rev(yFromRow(im)), z = t(as.matrix(im)[nrow(im):1, ]))
image(list_image)
It's slow, and though it calls the source of 'rect()' under the hood, we can't also set the border colour. Use 'useRaster = TRUE' to use 'rasterImage' for more efficient drawing time, control over interpolation, and ultimately - file size.
Now let's plot the image again, but by explicitly calling rect for every pixel. ('quadmesh' probably not the easiest way to demonstrate, it's just fresh in my mind).
## worker function to plot rect from vertex index
rectfun <- function(x, vb, ...) rect(vb[1, x[1]], vb[2,x[1]], vb[1,x[3]], vb[2,x[3]], ...)
## draw just the borders on the original, traditional image
apply(msh$ib, 2, rectfun, msh$vb, border = "white")
Now try again with 'rect'.
## redraw the entire image, with rect calls
##(not efficient, but essentially the same as what image does with useRaster = FALSE)
cols <- heat.colors(12)
## just to clear the plot, and maintain the plot space
image(im, col = "black")
for (i in seq(ncol(msh$ib))) {
rectfun(msh$ib[,i], msh$vb, col = cols[scl(im[i]) * (length(cols)-1) + 1], border = "dodgerblue")
}

Annotate ggplot2 graphs using tikzAnnotate in tikzDevice

I would like to use tikzDevice to include annotated ggplot2 graphs in a Latex document.
tikzAnnotate help has an example of how to use it with base graphics, but how to use it with a grid-based plotting package like ggplot2? The challenge seems to be the positioning of the tikz node.
playwith package has a function convertToDevicePixels (http://code.google.com/p/playwith/source/browse/trunk/R/gridwork.R) that seems to be similar to grconvertX/grconvertY, but I am unable to get this to work either.
Would appreciate any pointers on how to proceed.
tikzAnnotate example using base graphics
library(tikzDevice)
library(ggplot2)
options(tikzLatexPackages = c(getOption('tikzLatexPackages'),
"\\usetikzlibrary{shapes.arrows}"))
tikz(standAlone=TRUE)
print(plot(15:20, 5:10))
#print(qplot(15:20, 5:10))
x <- grconvertX(17,,'device')
y <- grconvertY(7,,'device')
#px <- playwith::convertToDevicePixels(17, 7)
#x <- px$x
#y <- px$y
tikzAnnotate(paste('\\node[single arrow,anchor=tip,draw,fill=green] at (',
x,',',y,') {Look over here!};'))
dev.off()
Currently, tikzAnnotate only works with base graphics. When tikzAnnotate was first written, the problem with grid graphics was that we needed a way of specifying the x,y coordinates relative to the absolute lower left corner of the device canvas. grid thinks in terms of viewports and for many cases it seems the final coordinate system of the graphic is not known until it is heading to the device by means of the print function.
It would be great to have this functionality, but I could not figure out a way good way to implement it and so the feature got shelved. If anyone has details on a good implementation, feel free to start a discussion on the mailing list (which now has an alternate portal on Google Groups) and it will get on the TODO list.
Even better, implement the functionality and open a pull request to the project on GitHub. This is guaranteed to get the feature into a release over 9000 times faster than if it sits on my TODO list for months.
Update
I have had some time to work on this, and I have come up with a function for converting grid coordinates in the current viewport to absolute device coordinates:
gridToDevice <- function(x = 0, y = 0, units = 'native') {
# Converts a coordinate pair from the current viewport to an "absolute
# location" measured in device units from the lower left corner. This is done
# by first casting to inches in the current viewport and then using the
# current.transform() matrix to obtain inches in the device canvas.
x <- convertX(unit(x, units), unitTo = 'inches', valueOnly = TRUE)
y <- convertY(unit(y, units), unitTo = 'inches', valueOnly = TRUE)
transCoords <- c(x,y,1) %*% current.transform()
transCoords <- (transCoords / transCoords[3])
return(
# Finally, cast from inches to native device units
c(
grconvertX(transCoords[1], from = 'inches', to ='device'),
grconvertY(transCoords[2], from = 'inches', to ='device')
)
)
}
Using this missing piece, one can use tikzAnnotate to mark up a grid or lattice plot:
require(tikzDevice)
require(grid)
options(tikzLatexPackages = c(getOption('tikzLatexPackages'),
"\\usetikzlibrary{shapes.arrows}"))
tikz(standAlone=TRUE)
xs <- 15:20
ys <- 5:10
pushViewport(plotViewport())
pushViewport(dataViewport(xs,ys))
grobs <- gList(grid.rect(),grid.xaxis(),grid.yaxis(),grid.points(xs, ys))
coords <- gridToDevice(17, 7)
tikzAnnotate(paste('\\node[single arrow,anchor=tip,draw,fill=green,left=1em]',
'at (', coords[1],',',coords[2],') {Look over here!};'))
dev.off()
This gives the following output:
There is still some work to be done, such as:
Creation of a "annotation grob" that can be added to grid graphics.
Determine how to add such an object to a ggplot.
These features are scheduled to appear in release 0.7 of the tikzDevice.
I have made up a small example based on #Andrie's suggestion with geom_text and geom_polygon:
Initializing your data:
df <- structure(list(x = 15:20, y = 5:10), .Names = c("x", "y"), row.names = c(NA, -6L), class = "data.frame")
And the point you are to annotate is the 4th row in the dataset, the text should be: "Look over here!"
point <- df[4,]
ptext <- "Look over here!"
Make a nice arrow calculated from the coords of the point given above:
arrow <- data.frame(
x = c(point$x-0.1, point$x-0.3, point$x-0.3, point$x-2, point$x-2, point$x-0.3, point$x-0.3, point$x-0.1),
y = c(point$y, point$y+0.3, point$y+0.2, point$y+0.2, point$y-0.2, point$y-0.2, point$y-0.3, point$y)
)
And also make some calculations for the position of the text:
ptext <- data.frame(label=ptext, x=point$x-1, y=point$y)
No more to do besides plotting:
ggplot(df, aes(x,y)) + geom_point() + geom_polygon(aes(x,y), data=arrow, fill="green") + geom_text(aes(x, y, label=label), ptext) + theme_bw()
Of course, this is a rather hackish solution, but could be extended:
compute the size of arrow based on the x and y ranges,
compute the position of the text based on the length of the text (or by the real width of the string with textGrob),
define a shape which does not overlaps your points :)
Good luck!

Resources