saving rotate images in R - avoiding background and maintaining size

1.5k views Asked by At

I have below code which is based upon my earlier question. When I save the image SaveThisPlot.png, it creates unwanted background and the image size also changes (it reduces). How can i save the image such that the rotated part is of exactly same size as earlier (77 rows and 101 columns) and there is no background?

library(raster)
r1 <- brick(system.file("external/rlogo.grd", package="raster"))
r1
x <- crop(r1, extent(0,ncol(r1),0,nrow(r1)))
plotRGB(x)

x1 <- 0:ncol(x)
y1 <- 0:nrow(x)
z <- matrix(1, nrow=length(x1), ncol=length(y1))

col.mat <- t(apply(matrix(rgb(getValues(x)/255), nrow=nrow(x), byrow=TRUE), 2, rev))

# Rotate 45 degrees
persp(x1, y1, z, zlim=c(0,2), theta = 20, phi = 90, 
      col = col.mat, scale=FALSE, border=NA, box=FALSE)
png("SaveThisPlot.png")
persp(x1, y1, z, zlim=c(0,2), theta = 20, phi = 90, 
      col = col.mat, scale=FALSE, border=NA, box=FALSE)
dev.off()

enter image description here

1

There are 1 answers

3
Jota On BEST ANSWER

You can use a transparent background, but rotating the plot will require re-sizing.

When you save your plot you can set png("SaveThisPlot.png", bg="transparent") to prevent a background from showing. Try:

png("SaveThisPlot.png", width=101 height=77, units="px", pointsize=1, bg="transparent")

Notice if you try to do this with a rotated version of the logo, the logo is re-sized to fit within the 101 x 77 pixel png. If you want to rotate it and maintain the logo size, you'll have to resize the png, ass @RomanLuštrik mentioned in his comment. Re-sizing, if that is an option for you, is just a matter of trigonometry to find the appropriate height and width parameters.

You can try setting new heights and widths to save the png using some trigonometry. Here is an example with 35 degrees of rotation:

# hypotenuse
hypot <- sqrt((nrow(x)/2)^2 + (ncol(x)/2)^2) 
# angle
angle <- asin((nrow(x)/2)/ hypot)/(pi/180)

width <- 2 * max(abs(hypot * cos((angle + 35) * pi/180)), 
                 abs(hypot * cos((- angle + 35) * pi/180)))

height <- 2 * max(abs(hypot * sin((angle + 35) * pi/180)), 
                 abs(hypot * sin((- angle + 35) * pi/180)))


png("SaveThisPlot.png", height=height, width=width, units="px", pointsize=1,
    bg="transparent")
par(mar=c(0,0,0,0), xaxs = "i", yaxs = "i")
persp(x1, y1, z, zlim=c(0,2), theta = 35, phi = 90, 
      col = col.mat, scale=FALSE, border=NA, box=FALSE)
dev.off()

enter image description here

The overall image will be not be 77 x 101 pixels, but the logo itself should be the same size, though a bit rugged. For a smoother image, see the rgl answer below, but note that a transparent background isn't available for rgl

Alternative approach using the rgl package:

A different approach to producing the image might be better if the end goal is to maintain the size and smoothness of the image. You can try the rgl (R-to-OpenGL) package and the persp3d function, where you can rotate the plot within its window without re-sizing the window or altering the proportions of the logo. You can mostly use what you've already got, but the color matrix needs to be modified to work with persp3d:

col.mat2 <- cbind(rbind(1, col.mat), 1)

Then, set the scene, call persp3d, rotate it, and save.

open3d() 
rgl.pop("lights") 
light3d(specular="black") # to reduce reflection
persp3d(x1,y1,z, col=col.mat2, ylab="", xlab="", zlab="",
        axes=FALSE, smooth=FALSE, aspect="iso")

# Rotate it by defining the userMatrix, then save    
par3d(userMatrix = rotationMatrix(0*pi/180, 0, 0, 1))
rgl.snapshot("Rlogo_0.png") # to save

par3d(userMatrix = rotationMatrix(45*pi/180, 0, 0, 1))
rgl.snapshot("Rlogo_45.png")

par3d(userMatrix = rotationMatrix(135*pi/180, 0, 0, 1))
rgl.snapshot("Rlogo_135.png")

RlogoRlogo_45degreesRlog_135degrees

To force this image into a smaller window, you can manipulate the size of the rgl display window and the amount of background using a combination of the windowRect and zoom parameters (See this question and answer, for example). However, a transparent background isn't an option with rgl currently.

open3d()
bg3d(color="#FF00FF") # pink background (some color not in the actual image)
rgl.pop("lights") 
light3d(specular="black") 
persp3d(x1,y1,z, col=col.mat2, axes=FALSE,
     ylab="", xlab="", zlab="", smooth=FALSE, aspect="iso")
rgl.viewpoint(zoom=0.52)
par3d(userMatrix = rotationMatrix(0*pi/180, 0, 0, 1), windowRect=c(0,0, 101, 77))
rgl.snapshot("Rlogo_77h124w.png")
par3d(userMatrix = rotationMatrix(45*pi/180, 0, 0, 1), windowRect=c(0,0, 101, 77))
rgl.snapshot("Rlogo45_77h124w.png")

I wasn't able to make the window any less than 124 pixels wide. It seems there is a minimum width of 124 pixels to an rgl window on my computer, as I can't make the window any smaller (i.e. can't collapse the minimize, restore, and close buttons on top of the window).

enter image description here enter image description here

To get a transparent background, you could import these saved pngs, and assign alpha values of 1 to the background color ("#FF69B4"):

library(png)
add.alpha <- readPNG("Rlogo_77h124w.png") #Try a rotate version, too

library(abind)
add.alpha <- abind(add.alpha, matrix(1, nrow=nrow(add.alpha), ncol=ncol(add.alpha)))

add.alpha[,,4][which(rgb(add.alpha[,,1],add.alpha[,,2],add.alpha[,,3]) == "#FF00FF")] <- 0
writePNG(add.alpha, "Rlogo_77h124w_alpha.png")

It turns out this method isn't perfect, and you may end up with a slight border on rotated images where the color isn't exactly "#FF00FF". You can address the border one way or another, if it is a problem for you, but it may not make a difference depending on the end goals. Try using add.alpha[,,4][which(add.alpha[,,1] > .9 & add.alpha[,,3] > .9 & add.alpha[,,2] < .85 )] <- 0 before writePNG

enter image description hereenter image description hereenter image description here

If you don't want the image corners cut off like above, then use the trigonometry from earlier to help size the window properly within par3d:

open3d(); bg3d(color="#FF69B4"); rgl.pop("lights"); light3d(specular="black") 
persp3d(x1,y1,z, col=col.mat2, axes=FALSE,
     ylab="", xlab="", zlab="", smooth=FALSE, aspect="iso")
view3d(zoom=.862)
par3d(userMatrix = rotationMatrix(-45*pi/180, 0, 0, 1), windowRect=c(0,0, width, height)) # width and height from trigonometry above
rgl.snapshot("Rlogo45_77h124w.png")
#library(png)
add.alpha <- readPNG("Rlogo45_77h124w.png") # Try "Rlogo_77h124w.png", too
#library(abind)
add.alpha <- abind(add.alpha, matrix(1, nrow=nrow(add.alpha), ncol=ncol(add.alpha)))

add.alpha[,,4][which(add.alpha[,,1] > .9 & add.alpha[,,3] > .9 & add.alpha[,,2] < .85 )] <- 0 # not a perfect solution
writePNG(add.alpha, "Rlogo45_77h124w_alpha.png")

enter image description here