/
a04-figurative-mosaics-variable-width.Rmd
304 lines (259 loc) · 12.7 KB
/
a04-figurative-mosaics-variable-width.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
---
title: "Figurative mosaics with variable-width lines"
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
```
An earlier [article](https://paezha.github.io/truchet/articles/a03-figurative-mosaics.html) in this series shows how to use flexible Truchet tiles to create figurative mosaics. Another way to create figurative mosaics is to vary the width of the lines. This article illustrate this procedure.
The keys to this procedure are to:
1. Generate a mosaic with a relatively dense collection of lines;
2. Slice these lines using a relatively fine grid; and
3. Use an image with a relatively high resolution, similar to the grid used to slice the lines of the mosaic.
Here, the word "relatively" is doing some heavy-lifting, and some experimentation will typically be required to find combinations of density of lines and resolution of the image that work well for a given mosaic.
The packages needed (addition to {truchet}), are [{dplyr}](https://dplyr.tidyverse.org/index.htm), [{ggplot2}](https://ggplot2.tidyverse.org/), [{imager}](https://dahtah.github.io/imager/imager.html), [lwgeom](https://r-spatial.github.io/lwgeom/), [{purrr}](https://purrr.tidyverse.org/), and [{sf}](https://r-spatial.github.io/sf/):
```{r load-packages, message=FALSE}
library(dplyr)
library(ggplot2)
library(imager)
library(lwgeom)
library(purrr)
library(sf)
library(truchet)
```
Read the image for this example. Use `imager::load.image()`:
```{r read-image}
marilyn <- load.image(system.file("extdata",
"marilyn.jpg",
package = "truchet"))
```
The size of the image is 800-by-1200 pixels, and it is already in greyscale:
```{r inspect-image}
marilyn
```
This is the image:
```{r plot-image}
plot(marilyn)
```
The resolution is too high, which in addition to increasing the computational demands kind of defeats the purpose of a figurative mosaic. Experimenting with various image sizes I have found that working in the neighborhood of 60,000 pixels gives pleasing results in reasonably short times. This represents a square image of approximately 245 pixels by side. Resizing does not necessarily give an image of the desired size here because it depends on the interpolation algorith. Presently, scaling by a factor of $1/4$ gives good results:
```{r resize-image}
marilyn_rs <- imager::imresize(marilyn,
scale = 1/4,
interpolation = 6)
```
The image needs to be converted to a data frame and the y axis needs to be reversed:
```{r convert-to-grayscale-data-frame}
marilyn_df <- marilyn_rs %>%
as.data.frame() %>%
mutate(y = -(y - max(y)))
```
Notice that now the size of the image is `r nrow(marilyn_df)`.
This is the plot of the rescaled image (using {ggplot}):
```{r plot-image-data-frame}
ggplot() +
geom_point(data = marilyn_df,
aes(x = x,
y = y,
color = value)) +
coord_equal()
```
Next, we need to create a data frame for placing the tiles for the mosaic. This presents a challenge: 60,000 tiles is too many - but at the same time, fewer pixels degrade the image. To resolve this, we can use a smaller mosaic that then is scaled up to the size of the desired image. A parameter `s` here indicates the spacing of the tiles in the target image. For example, $s = 15$ here means that there will be one tile every fifteen pixels in the target image. Other parameters used in this chunk include: 1) expanding the coordinates of the scaled-down mosaic by 4 pixels in each direction, to give a buffer around the target image; 2) defining the scale of the tiles, which is set to 1 here (meaning each individual tile is of size 1-by-1). The tiles used are of type "dl" and "dr".
```{r design-mosaic}
# This will use a smaller subset of points to create the mosaic, which will then be rescaled
s <- 15
xlim <- c(min(marilyn_df$x)/s - 4, max(marilyn_df$x)/s + 4)
ylim <- c(min(marilyn_df$y)/s - 4, max(marilyn_df$y)/s + 4)
# Create a data frame with the coordinates for the tiles and define a scale parameter
m_1 <- expand.grid(x = seq(xlim[1], xlim[2], 1),
y = seq(ylim[1], ylim[2], 1)) %>%
mutate(tiles = sample(c("dl", "dr"), n(), replace = TRUE),
scale_p = 1)
```
Function `st_truchet_ms()` is used to assemble the mosaic according to the specifications coded in data frame `m_1`:
```{r assemble-mosaic}
m_1 <- st_truchet_ms(df = m_1)
```
This is the mosaic:
```{r plot-mosaic}
ggplot() +
geom_sf(data = m_1 %>% st_truchet_dissolve(),
aes(fill = color),
color = "white")
```
Notice the difference in the scale of the mosaic to the target image. Despite the relatively dense aspect, the density of lines of this mosaic is not very high when scaled up to the size of the target image. To increase the number of lines, the mosaic is first dissolved (i.e., the boundaries of the individual mosaics are deleted to create compact polygons of the same color), and then buffered. This is done twice (which I find gives good results). After buffering, some geometries may become empty and need to be removed:
```{r dissolve-and-buffer}
m_2 <- m_1 %>%
# Dissolve boundaries
st_truchet_dissolve() %>%
# Buffer the polygons
st_buffer(dist = -0.15) %>%
# Adjust the color field to distinguish it from the original polygons
mutate(color = color + 2)
# Remove empty geometries
m_2 <- m_2[!st_is_empty(m_2), , drop = FALSE]
```
The mosaics at the moment are composed of polygons. In this following chunk they are cast to "MULTILINESTRING" type (the original mosaic after dissolving the boundaries):
```{r}
m_1_lines <- m_1 %>%
st_truchet_dissolve() %>%
st_cast(to = "MULTILINESTRING")
m_2_lines <- m_2 %>%
st_cast(to = "MULTILINESTRING")
```
This is now the mosaic composed of lines instead of polygons:
```{r mosaic-with-buffers}
ggplot() +
geom_sf(data = m_1_lines,
color = "red") +
geom_sf(data = m_2_lines,
color = "blue")
```
The mosaic, which is still a scaled down version of the target image, needs to be scaled up. To do this, we first get the union of the geometries so that they are scaled uniformly:
```{r union-of-geometries}
m_1_union <- st_union(m_1)
m_2_union <- st_union(m_2)
```
Recall that to make the mosaic we used only one pixel of every fifteen in the target image. We scale by an identical factor (and convert the result to simple features):
```{r scale-mosaic}
m_1_union <- (m_1_lines * s) %>%
st_sf()
m_2_union <- (m_2_lines * s) %>%
st_sf()
```
The mosaic is now in the same scale as the image, and the tiles instead of being 1-by-1 are now 15-by-15:
```{r plot-scaled-mosaic}
ggplot() +
geom_sf(data = m_1_union,
color = "red") +
geom_sf(data = m_2_union,
color = "blue")
```
We now bind the two sets of lines into the scaled up mosaic:
```{r bind-parts-of-mosaic}
mosaic <- rbind(m_1_union,
m_2_union)
```
The next step is to create a blade to slice the lines. We want to slice the lines into small segments, but there is a trade-off between their size (smaller segments give better defined mosaics) and the time it takes to complete the operation (slicing into smaller segments takes longer).
In any case, it does not make sense to slice the lines into segments smaller than the size of the pixels of the target image, because this would result in longer computer times with no gain in definition (as segments smaller than a pixel would get the same information when contained by the same pixel). Thus, we create a blade with a resolution of exactly one pixel (which is why rescaling the image is important, since besides assembling the mosaic slicing it is the second computational bottleneck in this procedure):
```{r create-blade}
# Use the bounding box of the mosaic to define the extents of the grid that becomes the blade
bbox <- st_bbox(mosaic) %>%
round()
# Create a data frame with the start and end points of the lines that become the blade to split the mosaic lines
blade <- data.frame(x_start = c(bbox$xmin:bbox$xmax,
rep(bbox$ymin,
length(bbox$ymin:bbox$ymax))),
x_end = c(bbox$xmin:bbox$xmax,
rep(bbox$xmax,
length(bbox$ymin:bbox$ymax))),
y_start = c(rep(bbox$ymin,
length(bbox$xmin:bbox$xmax)),
bbox$ymin:bbox$ymax),
y_end = c(rep(bbox$ymax,
length(bbox$xmin:bbox$xmax)),
bbox$ymin:bbox$ymax))
# Shift the blade a small amount to avoid perfect overlap with lines in the mosaic
blade <- blade %>%
mutate(across(everything(),
~ .x + 0.18))
# Create the blade and convert to simple features
blade <- purrr::pmap(blade,
function(x_start, x_end, y_start, y_end){
st_linestring(
matrix(c(x_start,
y_start,
x_end,
y_end),
ncol = 2,
byrow = TRUE))}) %>%
st_as_sfc()
```
We are now ready to use the blade to split the lines (warning: this will take several minutes, depending how fine the blade is):
```{r split-mosaic}
mosaic_lines <- mosaic %>%
st_split(blade)
```
Extract the geometries of the sliced lines:
```{r extract-geometries}
mosaic_lines <- mosaic_lines %>%
st_collection_extract(type = "LINESTRING") %>%
st_cast(to = "LINESTRING") %>%
mutate(id = 1:n())
```
Convert the data frame with the image to simple features. This way we can use functions from the {sf} package to find the nearest feature to borrow the greyscale values:
```{r convert-image-to-simple-features}
marilyn_sf <- marilyn_df %>%
st_as_sf(coords = c("x", "y"))
```
Find the nearest feature to each line segment and borrow the greyscale value:
```{r borrow-values-from-image}
value <- marilyn_sf[mosaic_lines %>%
st_nearest_feature(marilyn_sf),] %>%
pull(value)
```
We can now add the greyscale value to the data frame with the mosaic:
```{r add-values-to-mosaic}
mosaic_lines$value <- value
```
We are now ready to plot the mosaic.
A tricky aspect when rendering the image is how to achieve a good definition of the underlying image by changing the size of the lines. This may require manipulating the grayscale values. For example, in this plot the size is proportional to the value and the range of sizes of the lines varies between [0.01, 1]:
```{r render-figurative-mosaic}
ggplot() +
geom_sf(data = mosaic_lines %>%
st_set_agr("constant") %>%
st_crop(marilyn_sf),
# Reverse the valence of values
aes(size = -value)) +
scale_size(range = c(0.01, 1)) +
coord_sf(expand = FALSE) +
theme_void() +
theme(legend.position = "none",
plot.margin = margin(0.1, 0.1, 0.1, 0.1, "in"))
```
As an alternative to reversing the valence of values, we can exaggerate the differences in values with a negative exponential function, as in this example:
```{r render-figurative-mosaic-2}
ggplot() +
geom_sf(data = mosaic_lines %>%
st_set_agr("constant") %>%
st_crop(marilyn_sf),
aes(size = exp(-2 * value))) +
scale_size(range = c(0.01, 1)) +
coord_sf(expand = FALSE) +
theme_void() +
theme(legend.position = "none",
plot.margin = margin(0.1, 0.1, 0.1, 0.1, "in"))
```
An additional modification is to make the color of the lines a function of the greyscale value:
```{r render-figurative-mosaic-monotone}
ggplot() +
geom_sf(data = mosaic_lines %>%
st_set_agr("constant") %>%
st_crop(marilyn_sf),
aes(color = value,
size = exp(-2 * value))) +
scale_color_distiller(direction = -1) +
scale_size(range = c(0.01, 1)) +
coord_sf(expand = FALSE) +
theme_void() +
theme(legend.position = "none",
plot.margin = margin(0.1, 0.1, 0.1, 0.1, "in"))
```
Adding color to the background can help to emphasize the lines:
```{r render-figurative-mosaic-monotone-with-background}
ggplot() +
geom_sf(data = mosaic_lines %>%
st_set_agr("constant") %>%
st_crop(marilyn_sf),
aes(color = value,
size = exp(-2 * value))) +
scale_color_distiller(direction = -1) +
scale_size(range = c(0.01, 1)) +
coord_sf(expand = FALSE) +
theme_void() +
theme(legend.position = "none",
plot.margin = margin(0.1, 0.1, 0.1, 0.1, "in"),
plot.background = element_rect(fill = "azure"))
```
These are just some examples of the parameters that can be explored when creating figurative mosaics with Truchet tiles with variable-width lines.