- This is an exploratory look at extracting colours from Super Street Fighter 2 character sprites in order to make colour palettes.
- It’s just for fun and has some pretty serious flaws (and is also very inefficient)
- Executive summary: colours are hard
library(tidyverse)
library(magick)
- Start by writing a function that allows me to visualise a vector of colours
- This is for convenience and will be super useful later on
view_palette <- function(cols, lab_size = 3){
n <- length(cols)
nc <- ceiling(sqrt(n))
nr <- ceiling(n/nc)
n_extra <- (nc*nr) - n
v <- col2rgb(cols) %>% rgb2hsv() %>% magrittr::extract(3,)
tidyr::crossing(x=1:nc, y=1:nr) %>%
dplyr::arrange(desc(y), x) %>%
dplyr::mutate(pal = c(cols, rep("transparent", n_extra)),
v = c(v, rep(NA, n_extra)),
lab_col = dplyr::case_when(is.na(v) ~ "transparent",
v > 0.7 ~ "Black",
TRUE ~ "White")) %>%
ggplot2::ggplot()+
ggplot2::geom_tile(ggplot2::aes(x,y,fill=I(pal)), col=1)+
ggplot2::geom_text(ggplot2::aes(x,y,label=pal, col=I(lab_col)), size = lab_size)+
ggplot2::coord_equal()+
ggplot2::theme_void()+
ggplot2::theme(plot.caption = ggplot2::element_text(hjust = 0.5))+
ggplot2::labs(caption = paste0(n, " colours"))
}
# Test output
view_palette(viridis::turbo(9))
- Read a test image to develop some working code with
i <- image_read('files/vega.png')
image_ggplot(i)
- Convert the image to a tidy, long format raster dataframe (removing transparent pixels)
- Append the LAB colour model values in their own columns
# Convert to raster and compute LAB values
ir <-
i %>%
image_raster() %>%
as_tibble() %>%
filter(col != "transparent") %>%
transmute(col2rgb(col) %>%
t() %>%
farver::convert_colour(from = "rgb", to = "lab") %>%
as_tibble())
- Compute the distance of each colour to every other colour
- Use hierarchical clustering to extract 12 colours (I think these sprites only contain 15 colours anyway!)
clusts <- dist(ir, method = "maximum") %>% hclust(method = "complete")
# Compute colours from clusters
cols_df <-
ir %>%
mutate(group = cutree(clusts, k=12)) %>%
group_by(group) %>%
summarise(across(c(l,a,b), mean), .groups = "drop") %>%
select(-group)
# All colours
all_cols <- cols_df %>% farver::encode_colour(from = "lab")
view_palette(all_cols)
- Implement a very sketchy way of trimming the colours, by removing similar colours based on a threshold value
- Compute the distance matrix for the colours extracted from the image
- Make the diagonal infinity (so it will never be below threshold)
- Iterate through the colours removing colours that are within a
distance threshold of the current iteration
- This makes the order of both the iteration and the colours important
d <- cols_df %>% dist(method = "maximum")
m <- as.matrix(d)
diag(m) <- Inf
# Set a threshold value
threshold <- 22
# Loop through each column of matrix and remove columns that are a similar colour
# to the current colour (based on threshold)
for(j in colnames(m)){
if(!(j %in% colnames(m))){next}
remove <- names(m[,j][m[,j] < threshold])
if(all(!remove %in% colnames(m))){next}
m <- m[, -which(colnames(m) %in% remove)]
}
- View the trimmed colours
trimmed_cols <- all_cols[colnames(m) %>% as.integer()]
view_palette(trimmed_cols)
- Map the closest colour in the image back to the trimmed colours
- This should mean that the final colours contained in the palette are actual colours contained within the original image
trimmed_cols <-
trimmed_cols %>%
matrix() %>%
image_read() %>%
image_map(i) %>%
image_raster() %>%
pull(col)
view_palette(trimmed_cols)
- Finally, order the trimmed colours based on their frequency in the original image
trimmed_cols_in_order <-
i %>%
image_map(image_read(matrix(c(trimmed_cols, "transparent")))) %>%
image_raster() %>%
filter(col != "transparent") %>%
count(col) %>%
arrange(desc(n)) %>%
pull(col)
view_palette(trimmed_cols_in_order)
- Visualise the image, the palette, and the image made only from the colours in the palette
# Return a visual summary
patchwork::wrap_plots(
# Plot original
image_ggplot(i) +
labs(subtitle = "Original")+
theme(plot.subtitle = element_text(hjust = 0.5)),
# Plot palette
view_palette(trimmed_cols_in_order)+
labs(subtitle = "Palette")+
theme(plot.subtitle = element_text(hjust = 0.5)),
# Plot image made from the palette
i %>%
image_map(image_read(matrix(c(trimmed_cols_in_order, "transparent")))) %>%
image_ggplot() +
labs(subtitle = "From palette")+
theme(plot.subtitle = element_text(hjust = 0.5))
)
- Wrap the essence of the code above into a function
make_palette <- function(img, threshold = 28, max_cols = 25, dist_method = "maximum"){
# Read image
i_original <- image_read(img)
# Make image smaller if it's big - as dist() and clustering will only be possible
# on very small images. Use image_sample to keep original colours in resizing
if(image_info(i_original)$width > 100){
i <- i_original %>% image_sample(geometry = "100x")
} else {
i <- i_original
}
# Convert to raster and compute LAB values
ir <-
i %>%
image_raster() %>%
as_tibble() %>%
filter(col != "transparent") %>%
transmute(col2rgb(col) %>%
t() %>%
farver::convert_colour(from = "rgb", to = "lab") %>%
as_tibble())
# Compute distances and hierarchical cluster
clusts <- dist(ir, method = dist_method) %>% hclust(method = "complete")
# Compute colours from clusters
cols_df <-
ir %>%
mutate(group = cutree(clusts, max_cols)) %>%
group_by(group) %>%
summarise(across(c(l,a,b), mean), .groups = "drop") %>%
select(-group)
# All colours (max_cols) before trimming
all_cols <- cols_df %>% farver::encode_colour(from = "lab")
# Convert selected colours back to LAB and compute distances
# Convert distances to matrix and make diagonal Infinity
d <- cols_df %>% dist(method = dist_method)
m <- as.matrix(d)
diag(m) <- Inf
# Loop through each column of matrix and remove columns that are a similar colour
# to the current colour (based on threshold)
for(j in colnames(m)){
if(!(j %in% colnames(m))){next}
remove <- names(m[,j][m[,j] < threshold])
if(all(!remove %in% colnames(m))){next}
m <- m[, -which(colnames(m) %in% remove)]
}
# Trim the colours and map colours from the original image to them
# This should mean the colours in the final palette are colours contained in the original image
trimmed_cols <-
all_cols[colnames(m) %>% as.integer()] %>%
matrix() %>%
image_read() %>%
image_map(i) %>%
image_raster() %>%
pull(col)
# Order the trimmed colours based on how often they appar when mapped onto
# the original image
trimmed_cols_in_order <-
i %>%
image_map(image_read(matrix(c(trimmed_cols, "transparent")))) %>%
image_raster() %>%
filter(col != "transparent") %>%
count(col) %>%
arrange(desc(n)) %>%
pull(col)
# Return a visual summary
patchwork::wrap_plots(
# Plot original
image_ggplot(i_original) +
labs(subtitle = "Original")+
theme(plot.subtitle = element_text(hjust = 0.5)),
# Plot palette
view_palette(trimmed_cols_in_order)+
labs(subtitle = "Palette")+
theme(plot.subtitle = element_text(hjust = 0.5)),
# Plot image made from the palette
i_original %>%
image_map(image_read(matrix(c(trimmed_cols_in_order, "transparent")))) %>%
image_ggplot() +
labs(subtitle = "From palette")+
theme(plot.subtitle = element_text(hjust = 0.5))
)
}
- Run the function on all sprites with default values
- I think these sprites have less than 25 colours (the default cols_max value I chose) - so its really just the colour trimming choosing the palettes on these examples
make_palette('files/ryu.png')
make_palette('files/ehonda.png')
make_palette('files/blanka.png')
make_palette('files/guile.png')
make_palette('files/ken.png')
make_palette('files/chun-li.png')
make_palette('files/zangief.png')
make_palette('files/dhalsim.png')
make_palette('files/balrog.png')
make_palette('files/vega.png')
make_palette('files/sagat.png')
make_palette('files/bison.png')
make_palette('files/cammy.png')
make_palette('files/deejay.png')
make_palette('files/fei-long.png')
make_palette('files/t-hawk.png')
Here I compare the output of my code with some of the example colour
palettes created in the {colorfindr}
package readme
file
make_palette('https://www.movieart.ch/bilder_xl/tintin-et-milou-poster-11438_0_xl.jpg')
make_palette('http://www.coverbrowser.com/image/lucky-luke/5-1.jpg')
make_palette('http://www.gallery29.ie/images/posters/1171469398_DSC03889.jpg')