An Aside - The Penny Red
Although our main focus in using rayshader
’s 3D plotting capabilities will be to render 3D interactive maps, it’s worth bearing in mind that rayshader
can be used as a general purpose 3D visualisation tool.
So before we start rendering 3D maps, and before we are tempted to think that maps are the only thing we can plot with rayshader
, let’s visualise something completely different: the colour of a Penny Red stamp…
We can obtain an appropriate image file from Wikipedia and save a local copy of it:
= 'https://upload.wikimedia.org/wikipedia/commons/a/a4/PennyRed.jpeg'
image.url = 'PennyRed.jpeg'
pennyred_file
# Download the file from a specified web location to a specifically named file
download.file(image.url, pennyred_file)
3D Rendering of Colour Images Using rayshader
To use rayshader
to render the image file, we need to obtain some “elevation” levels that will project some attribute of the image into the vertical z dimension.
One obvious candidate is the RGB colour value (we might alternatively render just the red, green or blue components) mapped to a single elevation value by regarding it as a base 256 encoded value:
library(raster)
= jpeg::readJPEG(pennyred_file)
pennyred_image # Also: png::readPNG(png_file)
# Create a raster file from the image
= raster(pennyred_file)
pennyred
# Isolate the reg, green and blue components
= pennyred_image[,,1]
pennyred_red = pennyred_image[,,2]
pennyred_green = pennyred_image[,,3]
pennyred_blue
#https://www.maptiler.com/news/2019/03/rgb-encoded-elevation-data-in-maptiler-cloud/
# height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)
# Use the RGB values as a base 256 elevation encoding
# then reduce the height with a base value
# We should really calculate the base value rather than use a
# value determined by observation...
# The values() function sets the value of the pennyred raster
values(pennyred) = -16700 + (((255-pennyred_red ) * 256 * 256 +
255-pennyred_green ) * 256 +
(255-pennyred_blue)) * 0.001)
(
# The pennyred raster now has values that encode RGB-as-elevation
pennyred
## class : RasterLayer
## band : 1 (of 3 bands)
## dimensions : 234, 200, 46800 (nrow, ncol, ncell)
## resolution : 1, 1 (x, y)
## extent : 0, 200, 0, 234 (xmin, xmax, ymin, ymax)
## crs : NA
## source : memory
## names : PennyRed
## values : 11.42209, 58.69265 (min, max)
To create an elevation matrix from the data, we might consider scaling from the raw RGB values:
<- matrix(
elev_matrix_pennyred ::extract(pennyred, raster::extent(pennyred)),
rasternrow = ncol(pennyred), ncol = nrow(pennyred)
)
The rayshader
package takes a simple elevation matrix and renders it in 2 or 3 dimensional relief.
For example, here’s a simple 2D rendering:
library(rayshader)
%>%
elev_matrix_pennyred sphere_shade(texture = "desert") %>%
#add_overlay(pennyred_image) %>%
plot_map()
#plot_3d(elev_matrix_pennyred)
We can also add the original image back as an overlay. In this case, we lose the 3D effect:
= elev_matrix_pennyred %>%
rayshaded_penny_red sphere_shade(texture = "desert") %>%
add_overlay(pennyred_image)
%>%
rayshaded_penny_red plot_map()
However, if we view the elevated image in a 3D plot, we can see the elevation map far more clearly:
# Configuration settings to allow us to render the WebGL
options(rgl.useNULL = TRUE,
rgl.printRglwidget = TRUE)
::clear3d()
rgl
%>%
rayshaded_penny_red plot_3d(elev_matrix_pennyred)
::rglwidget() rgl
# knitr widget embed example via:
# https://github.com/Robinlovelace/geocompr/blob/master/08-mapping.Rmd
# maybe set options?
#knitr::opts_chunk$set(widgetframe_widgets_dir = 'widgets' )
# Save widget to a local file
# w_file = "penny_red_3d_widget.html"
# htmlwidgets::saveWidget(r, w_file)
# We then need to save the file to a URL to use:
# knitr::include_url(URL)
# Or just explicitly use an iframe to load the local file?
11.5.1 Rendering the image directly from ggplot2
Whilst we can try to work out our own method for creating elevation models from an image, its much easier to use another tool that rayshader
provides: the plot_gg()
function.
This function can render a 3D plot directly from a ggplot2
object using the color
or fill
aesthetic for the elevation values. All the mappings from color to elevation are handled automatically, as is the overlaying of the original image.
So how do we get an image into a ggplot2
object?
The imager
R package contains some handy utilities for working with images further.
For example, it allows is to straightforwardly plot an image loaded in directly from a file:
<- imager::load.image(pennyred_file)
i_imager plot(i_imager)
We can cast the image as a dataframe, where each row contains an x
and a y
co=ordinate, the colour channel, cc
and the color value
:
<- as.data.frame(i_imager)
i_df
head(i_df)
## x y cc value
## 1 1 1 1 0.8901961
## 2 2 1 1 0.8156863
## 3 3 1 1 0.7803922
## 4 4 1 1 0.7019608
## 5 5 1 1 0.7176471
## 6 6 1 1 0.7960784
With the data in a data frame form, we can the plot from it using ggplot()
in the normal way:
library(ggplot2)
<- imager::grayscale(i_imager) %>% as.data.frame
df
= ggplot(df,aes(x,y)) + geom_raster(aes(fill=value)) +
gg_pr scale_y_continuous(trans=scales::reverse_trans()) +
scale_fill_gradient(low="black",high="white") +
coord_fixed()
gg_pr
How about if we pass a full colour image to the ggplot()
?
library(dplyr) # for mutate
<- as.data.frame(i_imager, wide="c") %>%
df2 mutate(rgb.val=rgb(c.1,c.2,c.3))
= ggplot(df2, aes(x,y)) +
gg_pr2 geom_raster(aes(fill=rgb.val)) +
scale_fill_identity() +
coord_fixed() +
scale_y_continuous(trans=scales::reverse_trans())
gg_pr2
Rendering a Movie
How can we best show off the 3d rendering of the elevated image? One way is to plot the ggplot()
image as a 3D model using the rayshader::plot_gg()
function and then render a movie from it using a custom shooting script to drive the camera:
# Configuration settings to allow us to render the movie
options(rgl.useNULL = FALSE,
rgl.printRglwidget = FALSE)
library(av)
::rgl.open()
rgl::clear3d()
rgl
#Create the plot
plot_gg(gg_pr2, width=5, height=5, scale=250,
raytrace = FALSE)
# Interesting camera orbit for previewing 3d plots from ggplot
# Script from https://joeystanley.com/blog/3d-vowel-plots-with-rayshader
# which seems to have pinched it form a tutorial somewhere?
#render_movie('demo_pennyred.mp4')
# Set up the camera position and angle
= 30 + 60 * 1/(1 + exp(seq(-7, 20, length.out = 180)/2))
phivechalf = c(phivechalf, rev(phivechalf))
phivecfull = 0 + 60 * sin(seq(0,359,length.out = 360) * pi/180)
thetavec = 0.45 + 0.2 * 1/(1 + exp(seq(-5, 20, length.out = 180)))
zoomvec = c(zoomvec, rev(zoomvec))
zoomvecfull
# 3D movie
= 'demo_pennyred.mp4'
video_fn
# Actually render the video.
#render_movie(filename = video_fn, type = "custom",
# frames = 360, zoom = zoomvecfull,
# phi = phivecfull, theta = thetavec)
::rgl.close()
rgl
::embed_video(video_fn, width = "256", height = "256") embedr
So with that taste of what rayshader
can do, let’s get back to our rally data…