#1 SCALE IMAGE ----
# Adapted from LEGO mosaics project
<- function(image, img_size){
scale_image #Convert image to a data frame with RGB values
<- bind_rows(
img list(
as.data.frame(image[, , 1]) %>%
(mutate(y=row_number(), channel = "R")),
as.data.frame(image[, , 2]) %>%
(mutate(y=row_number(), channel = "G")),
as.data.frame(image[, , 3]) %>%
(mutate(y=row_number(), channel = "B"))
)%>%
) gather(x, value, -y, -channel) %>%
mutate(x = as.numeric(gsub("V", "", x))) %>%
spread(channel, value)
<- round(img_size, 0)
img_size
#Wide or tall image? Shortest side should be `img_size` pixels
if(max(img$x) > max(img$y)){
<- max(img$x) / max(img$y)
img_scale_x <- 1
img_scale_y else {
} <- 1
img_scale_x <- max(img$y) / max(img$x)
img_scale_y
}
#If only 1 img_size value, create a square image
if(length(img_size) == 1){
<- c(img_size, img_size)
img_size2 else {
} <- img_size[1:2]
img_size2 <- 1
img_scale_x <- 1
img_scale_y
}
#Rescale the image
<- img %>%
img2 mutate(y_scaled = (y - min(y))/(max(y)-min(y))*img_size2[2]*img_scale_y + 1,
x_scaled = (x - min(x))/(max(x)-min(x))*img_size2[1]*img_scale_x + 1) %>%
select(-x, -y) %>%
group_by(y = ceiling(y_scaled), x = ceiling(x_scaled)) %>%
#Get average R, G, B and convert it to hexcolor
summarize_at(vars(R, G, B), funs(mean(.))) %>%
rowwise() %>%
mutate(color = rgb(R, G, B)) %>%
ungroup() %>%
#Center the image
filter(x <= median(x) + img_size2[1]/2, x > median(x) - img_size2[1]/2,
<= median(y) + img_size2[2]/2, y > median(y) - img_size2[2]/2) %>%
y #Flip y
mutate(y = (max(y) - y) + 1)
<- list()
out_list "Img_scaled"]] <- img2
out_list[[
return(out_list)
}
How To: Spiral line drawings with the tidyverse and gganimate
Introduction
I stumbled on this video from Instagram of an artist creating a picture of Marilyn Monroe by drawing a single spiral from the inside out, varying the thickness of the line to add light and shadow to the image.
After watching the video on loop way too many times, I decided that I had to try and see if could do the same. Given that my drawing skills are near-zero, I turned to R and ggplot. Turns out, this was also a great opportunity to learn Thomas Pedersen’s gganimate package for turning ggplots into animated gifs.
The Goal
Using R, the tidyverse, and gganimate, reproduce a photo as a spiral using just a single line of varying thickness.
Prepare the image
Like the video, we’ll reproduce an iconic black-and-white image, though I chose a portrait of Albert Einstein instead.
As a first step, we resize the image. I had previously written some functions to convert an image into a LEGO mosaic. We can use the scale_image()
function from this project, which takes a 3-dimensional JPG or PNG matrix (width, height, RGB channel), crops it into a square, and converts it to a tidy data frame for plotting.
Click here to see script
We will want our final spiral to have a radius of 50px (radius
), so we can pass radius * 2
to the scaling function, creating a 100 pixel x 100 pixel image.
library(tidyverse)
library(jpeg)
<- 50 #pixels
radius <- readJPEG("Einstein.jpg") %>%
einstein scale_image(radius * 2)
$Img_scaled %>%
einsteinggplot(aes(x=x, y=y, fill=color)) +
geom_raster() +
scale_fill_identity(guide = FALSE) +
labs(title = "Scaled Einstein image",
subtitle = "100px * 100px") +
coord_fixed() +
theme_void()
Polar vs Cartesian coordinates
Drawing a spiral in polar coordinates is easy enough…
tibble(x = rep(c(1:20), 20), y = 1:400) %>%
ggplot(aes(x=x, y=y)) +
geom_path() +
coord_polar() +
theme_void()
Using that process, I originally tried to convert the image x- and y-values into polar coordinates beginning in the center of the image. That task turned out to be much more difficult than I had imagined.
Instead, I opted to draw a spiral in Cartesian coordinates. It’s been 10 years since my last trigonometry class, but found this helpful post on Stack Overflow. Based off the first answer on the thread, I wrote a function to calculate the points of a spiral centered on the image. All points on this spiral are equidistant, so more points are on the outer sections of the spiral than the inner sections.
Click here to see script
# Function for equidistant points on a spiral
<- function(img_df, spiral_radius, num_coils, chord_length, rotation){
spiral_cartesian <- img_df$Img_scaled
img
#Derive additional spiral specifications
<- median(img$x)
centerX <- median(img$y)
centerY
<- num_coils * 2 * pi
thetaMax <- spiral_radius / thetaMax
awayStep
#While loop to keep drawing spiral until we hit thetaMax
<- tibble()
spiral <- chord_length/awayStep
theta
while(theta <= thetaMax){
#How far away from center
= awayStep * theta
away
#How far around the center
= theta + rotation
around
#Convert 'around' and 'away' to X and Y.
= centerX + cos(around) * away
x = centerY + sin(around) * away
y
<- spiral %>%
spiral bind_rows(tibble(x=x, y=y))
= theta + chord_length/away
theta
}
return(c(img_df, list(spiral = spiral)))
}
We can then pass the einstein
image into this function (this provides the function with the desired x- and y-limits), along with specifications for the radius of the spiral (defined earlier), the number of coils, the chord length (distance between each point), and the rotation. We can plot this spiral using coord_fixed()
- Cartesian coordinates rather than the polar coordinates above.1
<- einstein %>%
einstein spiral_cartesian(spiral_radius = radius,
num_coils = 50, #Spiral folds on itself 50 times
chord_length = 2, #Each point is 2 pixels apart
rotation = 0 #No rotation
)
$spiral %>%
einsteinggplot(aes(x=x, y=y)) +
geom_path() +
coord_fixed() + #Not polar!
theme_void()
Mapping the image onto the spiral
The artist in the video is able to portray Marilyn Monroe by varying the thickness of the line while drawing a continuous spiral. Now that we have the our spiral in Cartesian coordinates and have scaled it to same size as the photo of Einstein, we need to vary the thickness.
We can do this by effectively overlaying the spiral on top of the image and then assigning the color of the closet image pixel(s) to the spiral point. In this function, I convert the three color channels to an inverted grey scale, where the value of 0 means white, 1 means black, and anything in between is a shade of grey.
Click here to see script
#Project the image onto the spiral
<- function(img_df){
project_image <- img_df$spiral %>%
dat #Round each spiral point to nearest whole number
mutate(xs = round(x), ys = round(y)) %>%
#Join on the rounded points
left_join(img_df$Img_scaled %>% rename(xs=x, ys=y)) %>%
#Create greyscale - 0 is lightest, 1 is darkest
mutate(grey = R+G+B,
grey = (1- (grey / max(grey))))
return(c(img_df, list(projected_spiral = dat)))
}
We plot the spiral again, but this time, use the grey value to scale the thickness2 of the line. To increase the contrast of the photo, raise the grey value to a power greater than 1.
<- einstein %>%
einstein project_image()
$projected_spiral %>%
einsteinggplot(aes(x=x, y=y, size = grey^(5/4))) +
geom_path() +
scale_size_continuous(range = c(0.1, 1.8),
guide = FALSE) +
coord_fixed(expand = FALSE) +
theme_void()
…Boom! A single spiral of varying line thickness to render an image of Albert Einstein.
Animating the spiral
The original video seems way more impressive than this image. I can’t compete with a hand-drawn image, but can add some drama to this project by animating the spiral, slowly drawing it from the inside out.
We can do this using the package gganimate, which converts ggplots into animated gifs3.
library(gganimate)
<- einstein$projected_spiral %>%
p mutate(row_number = row_number()^(1/2)) %>% #^(1/2) slows down the beginning of the drawing
ggplot(aes(x=x, y=y, size = grey)) + #Original contrast for this larger drawing
geom_path() +
scale_size_continuous(range = c(0.1, 1.8),
guide = FALSE) +
coord_fixed(expand = FALSE) +
theme_void() +
transition_reveal(row_number)
Adding gganimate
functions to an existing ggplot is simple. We add one new series, row_number
, to our data frame, which is used in the final plotting step. Adding + transition_reveal(1, row_number)
to the ggplot instructions tells R to render this as an animation, revealing all the data as a single group (1
) over the time span row_number
. By using the square root of row_number()
, gganimate
will spend more time drawing the inner sections of the spiral, speeding up as it reaches the outer lines.
Project: ✅ Done
Additional features
The spiral drawing function can be used for color images as well. This doesn’t make much sense for simulating a hand-drawn image using a marker, but still produces a fun image.
<- readJPEG("GoldenGirls.jpg") %>%
goldengirls scale_image(radius * 2) %>%
spiral_cartesian(spiral_radius = radius,
num_coils = 50, #Spiral folds on itself 50 times
chord_length = 2, #Each point is 2 pixels apart
rotation = 0 #No rotation
%>%
) project_image()
$projected_spiral %>%
goldengirlsmutate(row_number = row_number()^(1/2)) %>%
ggplot(aes(x=x, y=y, size = grey,
color = color)) +
geom_path(aes(group=1)) + #Add a group to tell it to draw a single line
scale_size_continuous(range = c(0.1, 1.8),
guide = FALSE) +
scale_color_identity(guide = FALSE) +
coord_fixed(expand = FALSE) +
theme_void() +
transition_reveal(1, row_number)
And finally, let’s end where we began, by rendering the image of Marilyn Monroe as an animated spiral.
<- readJPEG("Marilyn.jpg") %>%
marilyn scale_image(radius * 2) %>%
spiral_cartesian(spiral_radius = radius,
num_coils = 50, #Spiral folds on itself 50 times
chord_length = 2, #Each point is 2 pixels apart
rotation = 0 #No rotation
%>%
) project_image()
$projected_spiral %>%
marilynmutate(row_number = row_number()^(1/2)) %>%
ggplot(aes(x=x, y=y, size = grey)) +
geom_path() +
scale_size_continuous(range = c(0.1, 1.8),
guide = FALSE) +
coord_fixed(expand = FALSE) +
theme_void() +
transition_reveal(1, row_number)
Try it out! Full script can be found on GitHub!
Footnotes
The plotted spiral seems to have reflections like a vinyl record. I assume that’s from the rendering of the plot at this resolution.↩︎
The
range
values inscale_size_continuous()
will require a bit of trial & error and will depend on the final size of the plotted spiral.↩︎If you’re using a Windows OS with R 3.5, you might have some trouble installing this package. I recommend reinstalling RTools, referring to this thread, and using this script.↩︎