Home

Awesome

<!-- README.md is generated from README.Rmd. Please edit that file -->

isocuboids <img src="man/hex-logo.png" align="right" height="139"/>

<!-- badges: start -->

Lifecycle:
experimental

<!-- badges: end -->

An experimental and in development R package for the production of isometric pseudo 3-D images.

Pseudo, because (for images) the height of the cuboids can be mapped to different aspects of the pixel colour (or position), creating a ‘fake 3D’ effect which may have no correlation to any form of ‘real’ height. Shading of the cuboid faces is also done naively - it’s a simple reduction in brightness, so there is no clever physics going on here!

I am developing this project for fun and personal learning. If it’s useful to you in any way, that’s great news, but please bear the following in mind…

Overview

Images

library(tidyverse)
library(magick)
library(isocuboids)

Read an image

i <- 'https://tatianamowry.files.wordpress.com/2018/06/skull-dm.png'
image_read(i)
<img src="man/figures/README-unnamed-chunk-3-1.png" width="30%" />

Defaults

By default, images are resized to be 60 cuboids wide res = 60 and rendered as an isometric view. Cuboid heights are mapped to the brightness value of their corresponding pixel and scaled to range between 1 and 10 units high

cuboid_image(i)
<img src="man/figures/README-unnamed-chunk-4-1.png" width="75%" />

Orientation perspective

The orientation applied to the image before projection. This changes the perspective of the final output. Note that this is a transformation of the incoming image - the origin of the coordinate system is unchanged.

cuboid_image(i, orientation = 1)
cuboid_image(i, orientation = 2)
cuboid_image(i, orientation = 3)
cuboid_image(i, orientation = 4)

<img src="man/figures/README-unnamed-chunk-5-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-5-2.png" width="50%" /><img src="man/figures/README-unnamed-chunk-5-3.png" width="50%" /><img src="man/figures/README-unnamed-chunk-5-4.png" width="50%" />

Fill colour and shading

The fill colour and degree of side shading can be modified with the cuboid_fill and shading values

cuboid_image(i, cuboid_fill = hcl.colors(20, "viridis"))
cuboid_image(i, cuboid_fill = "antiquewhite")
cuboid_image(i, cuboid_fill = hcl.colors(20, "plasma"), shading = c(0, 0, 0.7))
cuboid_image(i, cuboid_fill = hcl.colors(20, "plasma"), shading = c(0, 0.7, 0))

<img src="man/figures/README-unnamed-chunk-6-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-6-2.png" width="50%" /><img src="man/figures/README-unnamed-chunk-6-3.png" width="50%" /><img src="man/figures/README-unnamed-chunk-6-4.png" width="50%" />

Height scale

The overall scaling of the cuboid height can be set with height_scale

cuboid_image(i, height_scale = c(0, 0))
cuboid_image(i, height_scale = c(0, 10))
cuboid_image(i, height_scale = c(30, 40))
cuboid_image(i, height_scale = c(40, 30), cuboid_fill = hcl.colors(20, "plasma"))

<img src="man/figures/README-unnamed-chunk-7-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-7-2.png" width="50%" /><img src="man/figures/README-unnamed-chunk-7-3.png" width="50%" /><img src="man/figures/README-unnamed-chunk-7-4.png" width="50%" />

Return data

For fine control, a dataframe of the projected coordinates and polygon groups/colours can be returned

df <- cuboid_image(i, return_data = TRUE, height_scale = c(1, 10))
head(df)
#> # A tibble: 6 × 21
#>       x     z col           r     g     b     h     s     v cuboid…¹     y face 
#>   <dbl> <dbl> <chr>     <int> <int> <int> <dbl> <dbl> <dbl>    <int> <dbl> <chr>
#> 1    59    59 #000000ff     0     0     0     0     0     0     3600     1 right
#> 2    59    59 #000000ff     0     0     0     0     0     0     3600     1 right
#> 3    59    59 #000000ff     0     0     0     0     0     0     3600     1 right
#> 4    59    59 #000000ff     0     0     0     0     0     0     3600     1 right
#> 5    59    59 #000000ff     0     0     0     0     0     0     3600     1 left 
#> 6    59    59 #000000ff     0     0     0     0     0     0     3600     1 left 
#> # … with 9 more variables: ux <dbl>, uy <dbl>, uz <dbl>, px <dbl>, py <dbl>,
#> #   pz <dbl>, v_adjusted <dbl>, col_adjusted <chr>, plot_group <fct>, and
#> #   abbreviated variable name ¹​cuboid_id
#> # ℹ Use `colnames()` to see all variable names

Filter rows and columns

# Slices in z
df |> 
    filter(z %in% c(40, 20)) |> 
    ggplot()+ 
    geom_polygon(aes(x = px, y = py, fill = I(col_adjusted), group = plot_group), col = NA)+
    coord_equal()

# A ring of coordinates
df |> 
    mutate(
        xn = x - max(x)/2,
        zn = z - max(z)/2,
        r = sqrt(xn^2 + zn^2)) |> 
    filter(between(r, 22, 24)) |> 
    ggplot()+ 
    geom_polygon(aes(x = px, y = py, fill = I(col_adjusted), group = plot_group), col = NA)+
    coord_equal()

<img src="man/figures/README-unnamed-chunk-9-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-9-2.png" width="50%" />

Highlight specific cuboid faces

df |> 
    mutate(
        xn = x - max(x)/2,
        zn = z - max(z)/2,
        r = sqrt(xn^2 + zn^2),
        col_adjusted = 
            case_when(
                y == max(y) & face == "top" ~ "red",
                y == max(y) & face == "left" ~ "red3",
                y == max(y) & face == "right" ~ "red4",
                between(r, 22, 24) & face == "top" ~ "blue",
                between(r, 22, 24) & face == "left" ~ "blue3",
                between(r, 22, 24) & face == "right" ~ "blue4",
                TRUE ~col_adjusted)) |> 
    ggplot() + 
    geom_polygon(
        aes(x = px, y = py, fill = I(col_adjusted), group = plot_group), 
        col = NA) +
    coord_equal()
<img src="man/figures/README-unnamed-chunk-10-1.png" width="75%" />

Split the plot through facetting

df |> 
    ggplot() + 
    geom_polygon(
        aes(x = px, y = py, fill = I(col_adjusted), group = plot_group), 
        col = NA) +
    coord_fixed()+
    facet_wrap(~z > 30, ncol=1)

df |> 
    ggplot() + 
    geom_polygon(
        aes(x = px, y = py, fill = I(col_adjusted), group = plot_group), 
        col = NA) +
    coord_fixed()+
    facet_wrap(~ x < 30 & y > 5, ncol = 1)

<img src="man/figures/README-unnamed-chunk-11-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-11-2.png" width="50%" />

Height mapping

Cuboid height is mapped from the image pixel values. By default, brightness v is used. Any function of the following parameters can be passed to height_map - red r - green g - blue b - hue h - saturation s - brightness v - x-position x - z-position z

i2 <- 
    image_read('https://www.r-project.org/logo/Rlogo.png') |> 
    image_background("white")

i2
<img src="man/figures/README-unnamed-chunk-12-1.png" width="20%" />

Various functions of the x and z coordinates

cuboid_image(i2, 
             height_map = sin(scales::rescale(x,c(0,4*pi))), 
             crop_square = FALSE,
             height_scale = c(1, 10))

cuboid_image(i2, 
             height_map = z^3,
             crop_square = FALSE,
             height_scale = c(1, 30), a1 = 80)

cuboid_image(i2, 
             height_map = ((x - (max(x)/2))^2) + ((z - (max(z)/2))^2),
             crop_square = FALSE, 
             height_scale = c(1, 30))

cuboid_image(i2, 
             crop_square = FALSE, 
             height_map = (x-(max(x)/2))^3, 
             height_scale = c(1, 50))

<img src="man/figures/README-unnamed-chunk-13-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-13-2.png" width="50%" /><img src="man/figures/README-unnamed-chunk-13-3.png" width="50%" /><img src="man/figures/README-unnamed-chunk-13-4.png" width="50%" />

Demonstration of mapping various different colour values to height.

# Create a 'rainbow' colour image
i3 <- 
    rep(viridis::turbo(100), each = 40) |> 
    matrix(ncol = 100) |> 
    image_read()

i3
<img src="man/figures/README-unnamed-chunk-14-1.png" width="30%" />

Map hue, red, green and blue to height

cuboid_image(i3, res = NULL, height_map = h, crop_square = FALSE)
cuboid_image(i3, res = NULL, height_map = r, crop_square = FALSE)
cuboid_image(i3, res = NULL, height_map = g, crop_square = FALSE)
cuboid_image(i3, res = NULL, height_map = b, crop_square = FALSE)

<img src="man/figures/README-unnamed-chunk-15-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-15-2.png" width="50%" /><img src="man/figures/README-unnamed-chunk-15-3.png" width="50%" /><img src="man/figures/README-unnamed-chunk-15-4.png" width="50%" />

Pre-edit with {magick}

Edit the image before passing into cuboid_image()

image_read(i) |> 
    image_resize("60x60") |> 
    image_motion_blur(angle = 45, radius = 6, sigma = 20) |> 
    cuboid_image(res = NULL)

image_read(i) |> 
    image_resize("60x60") |>
    image_canny() |> 
    cuboid_image(res = NULL, orientation = 2)

<img src="man/figures/README-unnamed-chunk-16-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-16-2.png" width="50%" />

Scan through an image

Scan through an image creating cross sectional plots

# Set aesthetics for visualisation
res <- 60
from <- 1 # height scale min
to <- 10 # height scale max

img <-
    image_read('https://tatianamowry.files.wordpress.com/2018/06/skull-dm.png') |> 
    image_resize(paste0(res, "x", res, "^")) |> 
    image_crop(paste0(res, "x", res), gravity = "center")

# Overall isometric angles plot
df1 <- 
    cuboid_image(
        img,
        res = NULL,
        height_scale = c(from, to),
        return_data = TRUE)

# Cross section 1
df2 <- 
    cuboid_image(
        img,
        res = NULL,
        height_scale = c(from, to),
        return_data = TRUE,
        a1 = 0, 
        a2 = 0,
        shading = c(0,0,0))

# Cross section 2
df3 <- 
    cuboid_image(
        img,
        res = NULL,
        height_scale = c(from, to),
        return_data = TRUE,
        a1 = 90, 
        a2 = 0, 
        shading = c(0,0,0))

for(i in df1$z |> unique() |> sort()){

    # Isometric plot
    p1 <-
        df1 |> 
        mutate(col_adjusted = case_when(
            z == i & x == i & face == "top" ~ "purple",
            z == i & x == i & face == "left" ~ "purple3",
            z == i & x == i & face == "right" ~ "purple4",
            z == i & face == "top" ~ "red",
            z == i & face == "left" ~ "red3",
            z == i & face == "right" ~ "red4",
            x == i & face == "top" ~ "blue",
            x == i & face == "left" ~ "blue3",
            x == i & face == "right" ~ "blue4",
            TRUE ~ col_adjusted)) |> 
        ggplot() +
        geom_polygon(aes(px, py, fill = I(col_adjusted), group = plot_group))+
        coord_equal()+
        theme(axis.title = element_blank())
    
    # Cross section 1
    p2 <-
        df2 |> 
        filter(z == i) |> 
        ggplot() +
        geom_polygon(aes(px, py, fill = I(col_adjusted), group = plot_group))+
        coord_equal(ylim = c(from, to))+
        theme(panel.border = element_rect(colour = "red", fill = NA, size = 1),
              axis.title = element_blank())
    
    # Cross section 2
    p3 <-
        df3 |> 
        filter(x == i) |> 
        ggplot() +
        geom_polygon(aes(px, py, fill = I(col_adjusted), group = plot_group))+
        coord_equal(ylim = c(from, to))+
        theme(panel.border = element_rect(colour = "blue", fill = NA, size = 1),
              axis.title = element_blank())
    
    # Output
    p4 <- patchwork::wrap_plots(p1, p2, p3, ncol =1)
    ggsave(paste0("data-raw/animation/",i,".jpg"), width = 6, height = 6, bg = "white")
}

# Read image files in correct order!
s <-
    tibble(f = list.files('data-raw/animation/', pattern = '.jpg', full.names = T)) |> 
    mutate(n = str_extract(f, "[0-9]+(?=\\.jpg)") |> as.integer()) |> 
    arrange(n) |> 
    pull(f) |> 
    image_read()

# Make smaller
s_small <- image_resize(s, "600x")

# Save animated gif
image_write_gif(s_small, 'data-raw/animation/anim.gif', delay = 0.15)
<img src="man/figures/README-unnamed-chunk-18-1.gif" width="75%" />

Matrices

For matrices, the matrix is not resized and the height of the cuboids is mapped directly to the values contained in the matrix.

cuboid_matrix(matrix(1))
cuboid_matrix(matrix(1:5), show_height_plane = TRUE)
cuboid_matrix(matrix(1:5), show_height_plane = TRUE, orientation = 4)
cuboid_matrix(matrix(seq(0,2,l=25), nrow=5), show_height_plane = TRUE, cuboid_col = 1)

<img src="man/figures/README-unnamed-chunk-19-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-19-2.png" width="50%" /><img src="man/figures/README-unnamed-chunk-19-3.png" width="50%" /><img src="man/figures/README-unnamed-chunk-19-4.png" width="50%" />

Some examples with the volcano data

cuboid_matrix(volcano |> scales::rescale(c(0,20)))
cuboid_matrix(volcano |> scales::rescale(c(20,0)))

<img src="man/figures/README-unnamed-chunk-20-1.png" width="50%" /><img src="man/figures/README-unnamed-chunk-20-2.png" width="50%" />

Generate terrain

Generate fake terrain using {ambient} noise. Shamelessly stolen from coolbutuseless

s <- 50
set.seed(s)

expand_grid(x=1:s, y=1:s) |>
    mutate(n = ambient::gen_perlin(x, y, frequency = 0.06)) |>
    pull(n) |> 
    cut(5, labels=FALSE) |>
    matrix(ncol = s) |> 
    cuboid_matrix(cuboid_fill = topo.colors(20))
<img src="man/figures/README-unnamed-chunk-21-1.png" width="75%" />

Penrose stairs

An approximation of the Penrose stairs. This is not exact, I used trial and error to get the correct spacing for the stairs to line up!

d <- seq(2, by=0.214, l=14)

m <-
    matrix(
        c(rev(d[1:6]),
          d[7], 0, 0, 0, 0, 0,
          d[8], 0, 0, 0, 0, 0,
          d[9], 0, 0, 0, 0, 0,
          d[10], 0, d[14], 0, 0, 0,
          d[11:13], 0, 0, 0), ncol = 6, byrow = T)

cuboid_matrix(m, cuboid_col = 1, show_axes = F, return_data = T) |>
    filter(!(x > 2 & z < 5)) |>
    ggplot()+
    geom_polygon(aes(px, py, group = plot_group, fill=face), col = 1)+
    coord_equal()+
    theme(legend.position = "")
<img src="man/figures/README-unnamed-chunk-22-1.png" width="75%" />